using System.Numerics; using AcDream.App.Rendering; using AcDream.App.UI; namespace AcDream.App.Rendering.Gpu.Vk; /// /// Campaign V slice V6d: the retained UI, drawn on Vulkan by the same /// the GL client draws it with. /// /// What this proves and what it does not. The whole retained stack /// runs here — walks a real widget tree, each widget draws /// through , and brackets /// it with /. /// Nothing about that path is Vulkan-aware. What it cannot be is the game's own /// UI: the retail widget tree is built from LayoutDesc and DAT chrome by /// TextureCache, which is still a GL type until slice V4t, so the sprites /// here are generated rather than decoded. The geometry of the exercise is what /// matters — every branch of ui_text.frag, the pixel-to-NDC mapping, and /// the top-left origin — and all three are visible in one screenshot. /// /// Deliberately asymmetric and labelled, for the same reason /// is: the one thing a symmetric layout could never /// prove is that the negative-viewport Y flip and the capture path agree. Widget /// rectangles are authored at known pixel coordinates from the TOP-LEFT corner, /// so a wrong flip puts the title bar at the bottom and is unmissable. /// internal sealed class VulkanRetainedUiScene : IDisposable { /// Where the header panel sits, in pixels from the top-left. Inspected in the capture. internal const int HeaderLeft = 24; internal const int HeaderTop = 18; internal const int HeaderWidth = 420; internal const int HeaderHeight = 96; private readonly MutableFrameSource _frames = new(); private readonly UiHost _host; private readonly DebugLineRenderer _lines; private readonly BitmapFont? _font; private readonly List _textures = []; private bool _disposed; internal VulkanRetainedUiScene(IGpuDevice device, string shaderDirectory) { ArgumentNullException.ThrowIfNull(device); byte[]? ttf = BitmapFont.TryLoadSystemMonospaceFont(); _font = ttf is null ? null : new BitmapFont(device, ttf, pixelHeight: 18f); _host = new UiHost(device, _frames, shaderDirectory, _font); _lines = new DebugLineRenderer(device, _frames, shaderDirectory); uint chrome = CreateTexture(device, "vk-ui-chrome", BuildChromeTile(), 16, 16, nearest: false); uint icon = CreateTexture(device, "vk-ui-icon", BuildIcon(), 32, 32, nearest: true); BuildTree(chrome, icon); } /// True when a system font was found; without one no glyph draws happen. internal bool HasFont => _font is not null; /// /// Draws one frame of the retained UI, plus a debug-line figure so both /// renderers ported this slice are exercised in the same frame. Each opens /// its own load/store pass against the backbuffer, exactly as they do in the /// GL client's HUD phase. /// internal void Render(IGpuFrame frame, uint width, uint height, double seconds) { ArgumentNullException.ThrowIfNull(frame); _frames.CurrentFrame = frame; try { // A 3-D figure through the second ported renderer. Identity view // with a plain orthographic-ish projection keeps it in NDC, which is // all this needs to demonstrate: the pipeline binds, the ring // allocation lands, and the push-constant matrix arrives. // Diagonal on purpose: an axis-aligned segment can land exactly on a // pixel boundary and rasterize sparsely, which reads as a bug in a // screenshot when it is only the diamond-exit rule. A slope makes // the evidence unambiguous, and the two segments are different // colours so the per-vertex attribute is visible too. _lines.Begin(); float sweep = (float)Math.Sin(seconds) * 0.05f; _lines.AddLine(new Vector3(-0.92f, -0.30f, 0f), new Vector3(-0.30f, -0.68f, 0f), new Vector3(1f, 0.85f, 0.2f)); _lines.AddLine(new Vector3(-0.30f, -0.68f, 0f), new Vector3(0.34f, -0.36f + sweep, 0f), new Vector3(0.2f, 1f, 0.6f)); _lines.Flush(Matrix4x4.Identity, Matrix4x4.Identity); _host.Tick(0.016); _host.Draw(new Vector2(width, height)); } finally { _frames.CurrentFrame = null; } } /// /// A header panel with a tiled sprite background, a nested solid panel with /// a border, two labels, and a loose icon. Between them these cover all /// three fragment branches: sprite modulate, flat vertex colour (both the /// rect bucket and DrawFill), and font coverage. /// private void BuildTree(uint chrome, uint icon) { var header = new UiPanel { Name = "vk-header", Left = HeaderLeft, Top = HeaderTop, Width = HeaderWidth, Height = HeaderHeight, BackgroundSprite = 1, SpriteResolve = _ => (chrome, 16, 16), }; header.AddChild(new UiLabel { Name = "vk-title", Left = 12, Top = 10, Width = 380, Height = 22, Text = "acdream - retained UI on Vulkan", TextColor = new Vector4(1f, 0.94f, 0.72f, 1f), }); header.AddChild(new UiLabel { Name = "vk-subtitle", Left = 12, Top = 34, Width = 380, Height = 22, Text = "top-left origin - slice V6d", TextColor = new Vector4(0.75f, 0.9f, 1f, 1f), }); var inner = new UiPanel { Name = "vk-inner", Left = 12, Top = 58, Width = 260, Height = 28, BackgroundColor = new Vector4(0.05f, 0.08f, 0.16f, 0.85f), BorderColor = new Vector4(0.6f, 0.75f, 1f, 1f), BorderThickness = 2f, }; inner.AddChild(new UiLabel { Left = 8, Top = 6, Width = 240, Height = 18, Text = "fill + border + glyphs", TextColor = new Vector4(1f, 1f, 1f, 1f), }); header.AddChild(inner); // Off to one side and near the bottom, so nothing about the layout is // mirror-symmetric in either axis. var badge = new UiTextureElement { Name = "vk-icon", Left = HeaderLeft + HeaderWidth + 16, Top = HeaderTop + 40, Width = 64, Height = 64, Texture = icon, }; _host.Root.AddChild(header); _host.Root.AddChild(badge); } private uint CreateTexture(IGpuDevice device, string name, byte[] rgba, int width, int height, bool nearest) { IGpuTexture texture = device.CreateTexture(new GpuTextureDescription( name, GpuTextureKind.Texture2D, GpuTextureFormat.Rgba8Unorm, width, height, LayerCount: 1, MipLevelCount: 1)); _textures.Add(texture); texture.Upload(0, 0, rgba); IGpuSampler sampler = device.CreateSampler(nearest ? GpuSamplerDescription.UiNearest : GpuSamplerDescription.WorldRepeat); return UiTextureTableHandle.FromSlot(device.RegisterTexture(texture, sampler)); } /// A 16x16 tile with a lit top-left corner, so tiling and orientation both read. private static byte[] BuildChromeTile() { const int extent = 16; var pixels = new byte[extent * extent * 4]; for (int y = 0; y < extent; y++) { for (int x = 0; x < extent; x++) { bool edge = x == 0 || y == 0; byte r = edge ? (byte)0xC8 : (byte)0x2A; byte g = edge ? (byte)0xA0 : (byte)0x24; byte b = edge ? (byte)0x40 : (byte)0x1C; int offset = ((y * extent) + x) * 4; pixels[offset + 0] = r; pixels[offset + 1] = g; pixels[offset + 2] = b; pixels[offset + 3] = 0xF0; } } return pixels; } /// A 32x32 badge: red at the top, blue at the bottom, opaque ring. private static byte[] BuildIcon() { const int extent = 32; var pixels = new byte[extent * extent * 4]; for (int y = 0; y < extent; y++) { for (int x = 0; x < extent; x++) { float dx = (x - 15.5f) / 15.5f; float dy = (y - 15.5f) / 15.5f; bool inside = (dx * dx) + (dy * dy) <= 1f; int offset = ((y * extent) + x) * 4; pixels[offset + 0] = (byte)(inside ? 255 - (y * 6) : 0); pixels[offset + 1] = (byte)(inside ? 60 : 0); pixels[offset + 2] = (byte)(inside ? y * 7 : 0); pixels[offset + 3] = (byte)(inside ? 255 : 0); } } return pixels; } public void Dispose() { if (_disposed) return; _disposed = true; _lines.Dispose(); _host.Dispose(); _font?.Dispose(); for (int i = _textures.Count - 1; i >= 0; i--) _textures[i].Dispose(); _textures.Clear(); } /// /// The host drives frames itself (it owns swapchain acquire and the /// out-of-date policy), so it hands the open frame to the renderers rather /// than the other way round. Same contract as /// GpuDeviceFrameLifetime, without owning the begin/end. /// private sealed class MutableFrameSource : ICurrentGpuFrameSource { public IGpuFrame? CurrentFrame { get; set; } } }