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; } }