diff --git a/src/AcDream.App/Rendering/RenderBootstrap.cs b/src/AcDream.App/Rendering/RenderBootstrap.cs
new file mode 100644
index 00000000..8f8a1001
--- /dev/null
+++ b/src/AcDream.App/Rendering/RenderBootstrap.cs
@@ -0,0 +1,162 @@
+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)
+{
+ ///
+ /// 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);
+ }
+}
+
+/// 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);
+
+ // --- 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);
+
+ return 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);
+ }
+
+ // 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;
+ }
+}