using System.Collections.Concurrent;
using DatReaderWriter;
using Microsoft.Extensions.Logging.Abstractions;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
///
/// The subset of the production render stack that the UI Studio needs:
/// GL + dats + UiHost + the WB mesh pipeline. Constructed from the same
/// classes and the same order as , minus
/// terrain / sky / physics / streaming.
///
public sealed record RenderStack(
GL Gl,
DatReaderWriter.DatCollection Dats,
string ShaderDir,
Wb.BindlessSupport Bindless,
TextureCache TextureCache,
Shader MeshShader,
Wb.WbMeshAdapter MeshAdapter,
Wb.EntitySpawnAdapter EntitySpawnAdapter,
Wb.WbDrawDispatcher DrawDispatcher,
SceneLightingUboBinding LightingUbo,
AcDream.App.UI.UiHost UiHost,
AcDream.App.UI.UiDatFont? VitalsDatFont,
AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable
{
/// Dispose the GL pieces this stack OWNS (everything created in
/// ). + are caller-owned
/// and NOT disposed here. Called once at studio teardown.
public void Dispose()
{
DrawDispatcher.Dispose();
MeshAdapter.Dispose();
TextureCache.Dispose();
MeshShader.Dispose();
LightingUbo.Dispose();
UiHost.Dispose();
}
///
/// Resolves a sprite id (0x06xxxxxx) to a (GL handle, width, height) triple.
/// Copied verbatim from GameWindow's ResolveChrome closure — it calls
/// TextureCache.GetOrUploadRenderSurface(id, out w, out h).
///
public (uint handle, int width, int height) ResolveChrome(uint spriteId)
{
uint t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h);
return (t, w, h);
}
// ── Font cache (per-stack, keyed by FontDid) ─────────────────────────────
///
/// Cache of loaded dat fonts keyed by FontDid (0x40000000-range).
/// Populated lazily by . Thread-safe for
/// concurrent reads from the studio render loop; writes happen only
/// during the first load of each distinct FontDid.
///
private readonly ConcurrentDictionary _fontCache = new();
///
/// Lazily load and cache a dat font by its FontDid. Returns null (and
/// caches null) when the Font DBObj is absent or has no foreground surface —
/// callers fall back to the global font in that case.
///
/// Pre-seeds (0x40000000) and
/// (0x40000001) from the already-loaded instances
/// to avoid a redundant upload on those two ids.
///
public AcDream.App.UI.UiDatFont? ResolveDatFont(uint fontDid)
{
return _fontCache.GetOrAdd(fontDid, id =>
AcDream.App.UI.UiDatFont.Load(Dats, TextureCache, id));
}
///
/// Pre-seeds the font cache from the two already-loaded font instances
/// (VitalsDatFont = 0x40000000, LargeDatFont = 0x40000001) so that
/// returns them without a redundant GL upload.
/// Called once by after the stack is
/// fully constructed.
///
internal void SeedFontCache()
{
if (VitalsDatFont is not null)
_fontCache.TryAdd(AcDream.App.UI.UiDatFont.DefaultFontId, VitalsDatFont);
if (LargeDatFont is not null)
_fontCache.TryAdd(0x40000001u, LargeDatFont);
}
}
/// Options for .
public sealed record RenderBootstrapOptions(
AcDream.UI.Abstractions.Settings.QualitySettings Quality);
///
/// Constructs the UI Studio's render stack from the production classes,
/// in the same order as .
///
public static class RenderBootstrap
{
///
/// Build the studio's render stack. Throws
/// (same message as GameWindow) if GL_ARB_bindless_texture or
/// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory.
///
public static RenderStack Create(
GL gl,
DatReaderWriter.DatCollection dats,
RenderBootstrapOptions opts)
{
// --- Bindless detection (GameWindow ~1701-1723) ---
if (!Wb.BindlessSupport.TryCreate(gl, out var bindless)
|| bindless is null
|| !bindless.HasShaderDrawParameters(gl))
{
throw new NotSupportedException(
"acdream requires GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters " +
"(GL 4.3+ with bindless support). Your GPU/driver does not expose these extensions. " +
"If this is unexpected, please file a bug report with your GPU vendor + driver version.");
}
// --- Shared infra (GameWindow ~1198, ~1211) ---
string shaderDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
var lightingUbo = new SceneLightingUboBinding(gl);
// --- Mesh shader (GameWindow ~1769-1771) ---
var meshShader = new Shader(gl,
Path.Combine(shaderDir, "mesh_modern.vert"),
Path.Combine(shaderDir, "mesh_modern.frag"));
// --- TextureCache (GameWindow ~1774) ---
var textureCache = new TextureCache(gl, dats, bindless);
// --- AnimLoader (GameWindow ~1240) ---
var animLoader = new AcDream.Core.Physics.DatCollectionLoader(dats);
// --- WbMeshAdapter (GameWindow ~2286-2287) ---
var wbLogger = NullLogger.Instance;
var meshAdapter = new Wb.WbMeshAdapter(gl, dats, wbLogger);
// --- SequencerFactory (GameWindow ~2306-2334) ---
var capturedDats = dats;
var capturedAnimLoader = animLoader;
AcDream.Core.Physics.AnimationSequencer SequencerFactory(AcDream.Core.World.WorldEntity e)
{
if (capturedDats is not null && capturedAnimLoader is not null)
{
var setup = capturedDats.Get(e.SourceGfxObjOrSetupId);
if (setup is not null)
{
uint mtableId = (uint)setup.DefaultMotionTable;
if (mtableId != 0)
{
var mtable = capturedDats.Get(mtableId);
if (mtable is not null)
return new AcDream.Core.Physics.AnimationSequencer(
setup, mtable, capturedAnimLoader);
}
// Setup exists but no motion table — no-op sequencer.
return new AcDream.Core.Physics.AnimationSequencer(
setup,
new DatReaderWriter.DBObjs.MotionTable(),
capturedAnimLoader);
}
}
// Complete fallback: empty setup + empty motion table + null loader.
return new AcDream.Core.Physics.AnimationSequencer(
new DatReaderWriter.DBObjs.Setup(),
new DatReaderWriter.DBObjs.MotionTable(),
new NullAnimLoader());
}
// --- EntitySpawnAdapter (GameWindow ~2335-2336) ---
var entitySpawnAdapter = new Wb.EntitySpawnAdapter(
textureCache, SequencerFactory, meshAdapter);
// --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) ---
var classificationCache = new Wb.EntityClassificationCache();
// --- WbDrawDispatcher (GameWindow ~2377-2381) ---
var drawDispatcher = new Wb.WbDrawDispatcher(
gl, meshShader, textureCache, meshAdapter, entitySpawnAdapter,
bindless, classificationCache);
drawDispatcher.AlphaToCoverage = opts.Quality.AlphaToCoverage;
// --- Vitals dat font (GameWindow ~1820-1822) ---
var vitalsDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache);
// --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text.
// The default font (0x40000000, 16px) renders the row names too small; the 18px
// variant (confirmed in client_portal.dat 2026-06-26) matches the retail character
// window list more closely (≈ icon height ≈ 24px target, 18px is best available).
var largeDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache, 0x40000001u);
// --- UiHost (GameWindow ~1790); pass null for debugFont (only used as
// a fallback BitmapFont for the world-space HUD — not needed for the
// UI Studio, and BitmapFont requires a system font byte array) ---
var uiHost = new AcDream.App.UI.UiHost(gl, shaderDir, defaultFont: null);
var stack = new RenderStack(
Gl: gl,
Dats: dats,
ShaderDir: shaderDir,
Bindless: bindless,
TextureCache: textureCache,
MeshShader: meshShader,
MeshAdapter: meshAdapter,
EntitySpawnAdapter: entitySpawnAdapter,
DrawDispatcher: drawDispatcher,
LightingUbo: lightingUbo,
UiHost: uiHost,
VitalsDatFont: vitalsDatFont,
LargeDatFont: largeDatFont);
// Pre-seed the font cache with the two already-uploaded atlas instances
// so ResolveDatFont(0x40000000) and ResolveDatFont(0x40000001) hit the cache
// rather than re-uploading the same GL texture a second time.
stack.SeedFontCache();
return stack;
}
// NullAnimLoader mirrors GameWindow's private NullAnimLoader (GameWindow ~13327-13330).
private sealed class NullAnimLoader : AcDream.Core.Physics.IAnimationLoader
{
public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) => null;
}
}