feat(render): draw the retained UI on Vulkan, and fix the pass it exposed
Campaign V slice V6d, commit 3 of 3 — the evidence commit, which turned out to also be a bug-fix commit.
VulkanBringUpHost now builds a real UiHost and DebugLineRenderer on the Vulkan device and draws them after the V6c verification scene, in their own single-sampled load/store passes against the backbuffer — the same shape the GL client's HUD phase has. Nothing in the retained stack is backend-aware: UiRoot walks a real widget tree, each widget draws through UiRenderContext, and UiHost.Draw brackets it with TextRenderer.Begin/Flush. What it cannot be is the game's own UI, because the retail tree is built from LayoutDesc and DAT chrome by TextureCache, which stays a GL type until V4t; the sprites here are generated instead. The widget rectangles are authored at known pixel offsets from the top-left and nothing is mirror-symmetric, so a wrong Y flip would put the title bar at the bottom.
The frame this produced was wrong, and usefully so. Whole runs of the debug-line figure were missing. Vulkan's rasterization-order guarantees are scoped to one render-pass instance; between two instances writing the same attachment there is no implicit ordering, and that includes a multisample RESOLVE, which is part of the render pass and therefore equally unordered against what follows. TransitionBackbufferForRendering emitted its acquire barrier once per frame and returned for every pass after the first, so the second and third passes raced the first one's resolve. V6c's frame had exactly one backbuffer pass and could not see this; V6d's has three. A later backbuffer pass now gets a colour-attachment dependency instead of nothing, and keeps ColorAttachmentOptimal as its old layout rather than Undefined, which would have licensed discarding everything drawn so far. Every line renders continuously afterwards.
Inspection of artifacts/vk-ui/vulkan-bringup.png against the authored layout, by pixel probe:
The header panel is authored at (24,18), 420x96. Its tiled chrome fills exactly x 24..443 and y 18..113 — one pixel outside on any edge is the clear colour. The tile's lit edge appears at the top and left of every cell, so texture row 0 lands at the top and the V axis is not flipped. Both labels read left to right, right side up, through the font-coverage branch. The nested panel's border samples exactly (153,191,255) against an authored (0.6,0.75,1.0), unblended — the untextured branch is bit-exact. The badge sprite is authored at (460,58), 64x64, and its gradient starts at x=460 with the clear colour at 455 — the RGBA-modulate branch, sampling a table slot. The two debug-line segments land on their computed screen coordinates. All three fragment branches, the pixel-to-NDC mapping, the top-left origin, straight-alpha blending and table sampling are therefore all confirmed on Vulkan, which is everything the offline GL gate confirms about the same code on GL.
The plan's V6 milestone is amended rather than claimed: "full game frame on Vulkan" is not reachable while V4c/V4d are parked and the world renderers and TextureCache are still raw GL, so V6 delivers the backend plus the two renderers that can use it today. The accumulated user-gate table gains a V6d row for the paperdoll/appraisal viewport sprite — the one retained-UI texture the offline scene never draws, on a slice that changed how every UI texture is sampled.
App tests 4,057 passed / 3 skipped, unchanged. Offline pixel gate against f6f58a12: differing fraction 3.20e-05, 18 pixels of 563,200, inside the documented 15-23 pixel noise band — as expected, since this commit touches only Vulkan files and the campaign doc.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f6f58a12db
commit
95f8c25f31
4 changed files with 323 additions and 12 deletions
|
|
@ -511,6 +511,7 @@ session, rather than one at a time:
|
||||||
| V4e | Particles (again) |
|
| V4e | Particles (again) |
|
||||||
| V4f | Sky — deliberately masked for determinism |
|
| V4f | Sky — deliberately masked for determinism |
|
||||||
| V4g | Paperdoll and appraisal viewports, portal transit |
|
| V4g | Paperdoll and appraisal viewports, portal transit |
|
||||||
|
| V6d | The paperdoll/appraisal viewport sprite. It is the one retained-UI texture the gate's scene never draws, and V6d changed how every UI texture is sampled — from a bound texture unit to a table slot. The seam that registers it (`GlGpuDevice.RegisterExternalColorTexture`) is unchanged and its handle is now simply encoded rather than resolved back to a GL name, but that path is unproven by anything automated. Check it with the dungeon/portal pass above rather than on its own |
|
||||||
|
|
||||||
**The user confirmed on 2026-07-27 that the local ACE server is always available
|
**The user confirmed on 2026-07-27 that the local ACE server is always available
|
||||||
and they will verify visually on request.** That converts this table from deferred
|
and they will verify visually on request.** That converts this table from deferred
|
||||||
|
|
@ -553,7 +554,7 @@ because sample positions are not specified across implementations.
|
||||||
| **V4g** ↻ **re-sequenced — §5.5.5** | `PrivateEntityViewportRenderer` → `IGpuRenderTarget`; `PortalDepthMaskRenderer` + `PortalTunnelPresentation` → stencil/depth-mask pipelines. | pixel gate incl. paperdoll and portal transit |
|
| **V4g** ↻ **re-sequenced — §5.5.5** | `PrivateEntityViewportRenderer` → `IGpuRenderTarget`; `PortalDepthMaskRenderer` + `PortalTunnelPresentation` → stencil/depth-mask pipelines. | pixel gate incl. paperdoll and portal transit |
|
||||||
| **V4h** ↻ **re-sequenced — §5.5.5** | Frame-spine formalization: pass executors emit real declared `BeginPass`/`EndPass` (clears and framebuffer management move out of the spine and into pass load/store ops), flight/screenshot/resize/profiler move onto the RHI, `OpenGLGraphicsDevice`'s live role retires, Chorizite consumers are audited, and the architecture test lands. **Milestone: seam complete.** | pixel + connected lifecycle + R6 soak + complete Release suite + interim perf (RHI-on-GL CPU p50 ≤ 1.95 ms) |
|
| **V4h** ↻ **re-sequenced — §5.5.5** | Frame-spine formalization: pass executors emit real declared `BeginPass`/`EndPass` (clears and framebuffer management move out of the spine and into pass load/store ops), flight/screenshot/resize/profiler move onto the RHI, `OpenGLGraphicsDevice`'s live role retires, Chorizite consumers are audited, and the architecture test lands. **Milestone: seam complete.** | pixel + connected lifecycle + R6 soak + complete Release suite + interim perf (RHI-on-GL CPU p50 ≤ 1.95 ms) |
|
||||||
| **V5** ✅ | Vulkan bring-up, dark: `ACDREAM_RENDER_BACKEND`, surface/instance/device/queues/swapchain, the capability record/probe/guard with the exit-4 contract, a clear-colour loop with screenshot and clean shutdown. | VK boots to clear on the RX 9070 XT; forced-unsupported knob → exit 4 |
|
| **V5** ✅ | Vulkan bring-up, dark: `ACDREAM_RENDER_BACKEND`, surface/instance/device/queues/swapchain, the capability record/probe/guard with the exit-4 contract, a clear-colour loop with screenshot and clean shutdown. | VK boots to clear on the RX 9070 XT; forced-unsupported knob → exit 4 |
|
||||||
| **V6** | Vulkan RHI backend, dark, three sequential commits: **a** allocator/buffers/staging/rings/timeline; **b** textures/BC mips/samplers/descriptor table/render targets/MSAA resolve; **c** `.spv` toolchain, pipelines, pipeline cache, negative viewport, push constants, timestamps, readback, debug names. **Milestone: full game frame on Vulkan.** | per-commit build + tests; VK renders world, UI, paperdoll, portals |
|
| **V6** | Vulkan RHI backend, dark, four sequential commits: **a** ✅ allocator/buffers/staging/rings/timeline (`fb9c6693`); **b** ✅ textures/BC mips/samplers/descriptor table/render targets/MSAA resolve (`9eae4963`); **c** ✅ `.spv` toolchain, pipelines, pipeline cache, negative viewport, push constants, timestamps, readback, debug names (`234fe91d`); **d** ✅ first production renderers — `TextRenderer` and `DebugLineRenderer` on both backends, the colour-format contract amendment, and the retained UI drawn on Vulkan. **Milestone deferred:** "full game frame on Vulkan" is not reachable while V4c/V4d are parked and the world renderers plus `TextureCache` are still raw GL, so V6 delivers the backend and the two renderers that can use it today. | per-commit build + tests; V6d additionally pixel-gates GL and captures a Vulkan UI frame |
|
||||||
| **V7** | GL-versus-Vulkan differential: `tools/run-backend-differential-gate.ps1`, strict paired-PNG compare, divergences fixed in the Vulkan backend only, then lifecycle + R6 soak natively on Vulkan, one validation-layer-clean run, one RenderDoc capture. **Milestone: parity.** | every differential checkpoint passes; both connected routes green on VK |
|
| **V7** | GL-versus-Vulkan differential: `tools/run-backend-differential-gate.ps1`, strict paired-PNG compare, divergences fixed in the Vulkan backend only, then lifecycle + R6 soak natively on Vulkan, one validation-layer-clean run, one RenderDoc capture. **Milestone: parity.** | every differential checkpoint passes; both connected routes green on VK |
|
||||||
| **V8** | Perf gate on the RX 9070 XT, uncapped, both backends, same route. | §2 acceptance table; parity is the floor |
|
| **V8** | Perf gate on the RX 9070 XT, uncapped, both backends, same route. | §2 acceptance table; parity is the floor |
|
||||||
| **V9** | Linux + CI: X11/Wayland surfaces; a `linux-vulkan` job on lavapipe (probe accepts on a real 1.3 software device, a short real render under xvfb, forced-unsupported → exit 4, `.spv` freshness). Physical Linux GPU row deferred post-cutover, as for Slice L. | CI green including the new job |
|
| **V9** | Linux + CI: X11/Wayland surfaces; a `linux-vulkan` job on lavapipe (probe accepts on a real 1.3 software device, a short real render under xvfb, forced-unsupported → exit 4, `.spv` freshness). Physical Linux GPU row deferred post-cutover, as for Slice L. | CI green including the new job |
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
||||||
// ── Campaign V slice V6c: the RHI backend and the scene that proves it ──
|
// ── Campaign V slice V6c: the RHI backend and the scene that proves it ──
|
||||||
private VulkanGpuDevice? _gpuDevice;
|
private VulkanGpuDevice? _gpuDevice;
|
||||||
private VulkanRhiScene? _scene;
|
private VulkanRhiScene? _scene;
|
||||||
|
private VulkanRetainedUiScene? _ui;
|
||||||
private VulkanDebugNames _debugNames = VulkanDebugNames.Disabled;
|
private VulkanDebugNames _debugNames = VulkanDebugNames.Disabled;
|
||||||
private VulkanDeviceFeatureSupport? _features;
|
private VulkanDeviceFeatureSupport? _features;
|
||||||
private VulkanDeviceLimitSupport? _limits;
|
private VulkanDeviceLimitSupport? _limits;
|
||||||
|
|
@ -394,6 +395,16 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
||||||
$"{sampleCount}x MSAA, pipeline cache " +
|
$"{sampleCount}x MSAA, pipeline cache " +
|
||||||
(_gpuDevice.PipelineCacheLoadedFromDisk ? "reused" : "cold") +
|
(_gpuDevice.PipelineCacheLoadedFromDisk ? "reused" : "cold") +
|
||||||
$", debug names {(_debugNames.IsEnabled ? "on" : "off")}");
|
$", debug names {(_debugNames.IsEnabled ? "on" : "off")}");
|
||||||
|
|
||||||
|
// Campaign V slice V6d: the first production renderers on Vulkan. The
|
||||||
|
// retained UI and the debug lines draw here through exactly the classes
|
||||||
|
// the GL client uses — the scene only supplies a widget tree and its
|
||||||
|
// sprites, because the retail tree's chrome still comes from a GL-only
|
||||||
|
// TextureCache until V4t.
|
||||||
|
_ui = new VulkanRetainedUiScene(_gpuDevice, ShaderSpirvDirectory());
|
||||||
|
_log(
|
||||||
|
"vulkan: retained UI up — TextRenderer and DebugLineRenderer on the Vulkan device" +
|
||||||
|
(_ui.HasFont ? string.Empty : " (no system font found; glyph draws are skipped)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Where the committed SPIR-V lives beside the binary.</summary>
|
/// <summary>Where the committed SPIR-V lives beside the binary.</summary>
|
||||||
|
|
@ -479,13 +490,15 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
VulkanSwapchainConfiguration configuration = _swapchain!.Configuration!;
|
VulkanSwapchainConfiguration configuration = _swapchain!.Configuration!;
|
||||||
|
double elapsed = (DateTimeOffset.UtcNow - started).TotalSeconds;
|
||||||
using (frame)
|
using (frame)
|
||||||
{
|
{
|
||||||
scene.Render(
|
scene.Render(frame, configuration.Width, configuration.Height, elapsed);
|
||||||
frame,
|
// After the 3-D scene, in its own single-sampled load/store pass
|
||||||
configuration.Width,
|
// against the backbuffer — the same shape the GL client's HUD
|
||||||
configuration.Height,
|
// phase has, and the reason the multisampled pass must resolve
|
||||||
(DateTimeOffset.UtcNow - started).TotalSeconds);
|
// rather than store.
|
||||||
|
_ui?.Render(frame, configuration.Width, configuration.Height, elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
_frameSerial = (ulong)frame.Serial;
|
_frameSerial = (ulong)frame.Serial;
|
||||||
|
|
@ -570,6 +583,8 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
||||||
// Scene before device: the scene owns buffers, textures, render
|
// Scene before device: the scene owns buffers, textures, render
|
||||||
// targets and pipelines whose release routes through the device's
|
// targets and pipelines whose release routes through the device's
|
||||||
// retirement queue, so the device has to still be alive to drain it.
|
// retirement queue, so the device has to still be alive to drain it.
|
||||||
|
_ui?.Dispose();
|
||||||
|
_ui = null;
|
||||||
_scene?.Dispose();
|
_scene?.Dispose();
|
||||||
_scene = null;
|
_scene = null;
|
||||||
_gpuDevice?.Dispose();
|
_gpuDevice?.Dispose();
|
||||||
|
|
@ -583,6 +598,8 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_ui?.Dispose();
|
||||||
|
_ui = null;
|
||||||
_scene?.Dispose();
|
_scene?.Dispose();
|
||||||
_scene = null;
|
_scene = null;
|
||||||
_gpuDevice?.Dispose();
|
_gpuDevice?.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -518,20 +518,49 @@ internal sealed unsafe partial class VulkanGpuDevice
|
||||||
_openPass = null;
|
_openPass = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the acquired swapchain image for a backbuffer pass.
|
||||||
|
///
|
||||||
|
/// <para>The FIRST pass of a frame acquires it: undefined contents, no prior
|
||||||
|
/// access to wait on, layout moved to colour-attachment.</para>
|
||||||
|
///
|
||||||
|
/// <para>Every pass AFTER that needs a dependency instead, and slice V6d is
|
||||||
|
/// where that started to matter. Vulkan's rasterization-order guarantees are
|
||||||
|
/// scoped to one render-pass instance; between two instances writing the same
|
||||||
|
/// attachment there is no implicit ordering at all, so the second one's draws
|
||||||
|
/// can land before or interleaved with the first one's colour writes — and
|
||||||
|
/// with the first one's multisample RESOLVE, which is part of the render pass
|
||||||
|
/// and therefore also unordered against what follows. V6c's frame had exactly
|
||||||
|
/// one backbuffer pass and could not see this. V6d's has three (world scene,
|
||||||
|
/// debug lines, retained UI), and the symptom was unmistakable once looked
|
||||||
|
/// at: whole runs of the debug-line figure missing where the earlier pass's
|
||||||
|
/// resolve had overwritten them, while the last pass's output survived
|
||||||
|
/// intact.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
private void TransitionBackbufferForRendering(CommandBuffer commands, Image image)
|
private void TransitionBackbufferForRendering(CommandBuffer commands, Image image)
|
||||||
{
|
{
|
||||||
if (_backbufferRenderingReady)
|
bool first = !_backbufferRenderingReady;
|
||||||
return;
|
|
||||||
_backbufferRenderingReady = true;
|
_backbufferRenderingReady = true;
|
||||||
|
|
||||||
var barrier = new ImageMemoryBarrier2
|
var barrier = new ImageMemoryBarrier2
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier2,
|
SType = StructureType.ImageMemoryBarrier2,
|
||||||
SrcStageMask = PipelineStageFlags2.TopOfPipeBit,
|
SrcStageMask = first
|
||||||
SrcAccessMask = AccessFlags2.None,
|
? PipelineStageFlags2.TopOfPipeBit
|
||||||
|
: PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||||
|
SrcAccessMask = first
|
||||||
|
? AccessFlags2.None
|
||||||
|
: AccessFlags2.ColorAttachmentWriteBit,
|
||||||
DstStageMask = PipelineStageFlags2.ColorAttachmentOutputBit,
|
DstStageMask = PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||||
DstAccessMask = AccessFlags2.ColorAttachmentWriteBit,
|
DstAccessMask = first
|
||||||
OldLayout = ImageLayout.Undefined,
|
? AccessFlags2.ColorAttachmentWriteBit
|
||||||
|
: AccessFlags2.ColorAttachmentWriteBit | AccessFlags2.ColorAttachmentReadBit,
|
||||||
|
// Undefined for the acquire — the contents are genuinely undefined
|
||||||
|
// and saying so lets the driver skip a decompress. A later pass in
|
||||||
|
// the same frame must NOT say Undefined: that would license
|
||||||
|
// discarding everything drawn so far.
|
||||||
|
OldLayout = first ? ImageLayout.Undefined : ImageLayout.ColorAttachmentOptimal,
|
||||||
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
||||||
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
|
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
|
||||||
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
|
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
|
||||||
|
|
|
||||||
264
src/AcDream.App/Rendering/Gpu/Vk/VulkanRetainedUiScene.cs
Normal file
264
src/AcDream.App/Rendering/Gpu/Vk/VulkanRetainedUiScene.cs
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.UI;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V6d: the retained UI, drawn on Vulkan by the same
|
||||||
|
/// <see cref="TextRenderer"/> the GL client draws it with.
|
||||||
|
///
|
||||||
|
/// <para><b>What this proves and what it does not.</b> The whole retained stack
|
||||||
|
/// runs here — <see cref="UiRoot"/> walks a real widget tree, each widget draws
|
||||||
|
/// through <see cref="UiRenderContext"/>, and <see cref="UiHost.Draw"/> brackets
|
||||||
|
/// it with <see cref="TextRenderer.Begin"/>/<see cref="TextRenderer.Flush"/>.
|
||||||
|
/// 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
|
||||||
|
/// <c>TextureCache</c>, 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 <c>ui_text.frag</c>, the pixel-to-NDC mapping, and
|
||||||
|
/// the top-left origin — and all three are visible in one screenshot.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Deliberately asymmetric and labelled</b>, for the same reason
|
||||||
|
/// <see cref="VulkanRhiScene"/> 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.</para>
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class VulkanRetainedUiScene : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>Where the header panel sits, in pixels from the top-left. Inspected in the capture.</summary>
|
||||||
|
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<IGpuTexture> _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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>True when a system font was found; without one no glyph draws happen.</summary>
|
||||||
|
internal bool HasFont => _font is not null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A 16x16 tile with a lit top-left corner, so tiling and orientation both read.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A 32x32 badge: red at the top, blue at the bottom, opaque ring.</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// <c>GpuDeviceFrameLifetime</c>, without owning the begin/end.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MutableFrameSource : ICurrentGpuFrameSource
|
||||||
|
{
|
||||||
|
public IGpuFrame? CurrentFrame { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue