diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index 29bb9f5a..ffed6621 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -132,7 +132,8 @@ ApplicationError1000 for exact PID26920 and LiveKernelEvent141 corroborate it.
The stdout graceful-logout lines are cleanup, NOT successful process exit.
RX9070XT, reported Vulkan1.4.349/driver2.0.395, final1600x900, High/4xMSAA.
-First raw acquire result is not retained: the swapchain adapter collapses
+At the original crash baseline, the first raw acquire result was not retained:
+the swapchain adapter collapsed
fatal failure and ordinary unavailability to false, then the empty signal
submission reports device loss. This is a diagnosis-obscuring boundary, not
proof of the initiating GPU fault. No premature-release or semaphore-reuse
@@ -169,6 +170,16 @@ reproduction. These short instrumented runs do not establish recurrence safety.
`research/2026-09-01-overhaul/477-cathedral-recurrence.md` records exact paths,
hashes and boundaries. #477/G4 remain open; FPS deferred, no production change.
+**First-failure reporting follow-up:** the bounded acquire/present boundary
+repair now throws the existing VulkanCallException for fatal results, preserving
+the original operation and result; normal unavailable/recreation cases remain
+unchanged. One independent API/production review PASS, six pre-fix failures,
+separate acquire/present sabotage failures, restored82/82, and lead integrated
+Release0W0E/17051pass/0fail/0skip. `research/2026-09-01-overhaul/477-first-failure-verification.md`.
+The prepared extended cathedral/Neftet run is next. This corrects error
+propagation, not the initiating AMD fault; #477/G4 remain open, FPS deferred.
+Windows denied access to the named watchdog dump; no ACL changes attempted.
+
## #476 — First post-resize graphical gate PNG is horizontally corrupted
**Status:** IN-PROGRESS — capture and resize-boundary repairs integrated,
diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md
index e0783246..1e894e51 100644
--- a/docs/architecture/acdream-architecture.md
+++ b/docs/architecture/acdream-architecture.md
@@ -1063,6 +1063,13 @@ device-buffer migration publishes prior transfer writes to its exact source
range before the migration read. Host-staged uploads remain covered by the one
batched trailing draw-visibility barrier rather than gaining per-copy barriers.
+Fatal native swapchain acquire/present results propagate at their call boundary
+as `VulkanCallException`, retaining the operation and exact result. They do not
+become ordinary unavailable/recreation outcomes or reach an abandoned-frame
+timeline submit after failed acquisition. Normal timeout, not-ready,
+out-of-date, and suboptimal handling remains owned by the existing swapchain
+policy. This preserves failure evidence; it is not device-loss recovery.
+
---
## Per-Frame Update Order (current runtime)
diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
index a80fca40..2b98c19f 100644
--- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
+++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
@@ -891,6 +891,16 @@ PASS provisional, nine PNGs inspected with control limits in verification
report. No running clients/debugger; ACE up. Owner wall acceptance pending.
No FPS work; AP159, AMD#477 and final G4 remain separate open tails.
+**#477 first-failure boundary follow-up:** contractc8ec04e97 preserves fatal
+acquire/present operation+VkResult before false/recreation can obscure it.
+One production file; normal policy, GPU synchronization/retirement, scene and
+FPS unchanged. Native pre-fix6expectedFAIL, separate sabotages3each, restored
+82/82 and independent API/production review PASS. Lead locked Release0W0E,
+17051/0/0 across14assemblies, manifest32/32. Exact DLLs and scope limits in
+`477-first-failure-verification.md`; prepared extended cathedral/Neftet run
+next. No AMD root-cause/stability clearance or new retail deviation. AP159,
+owner wall acceptance and G4 remain open; no main merge, FPS deferred.
+
## 14. Risk register (condensed)
- **R1 second scene graph.** Graph records own only topology/membership facts;
diff --git a/docs/research/2026-09-01-overhaul/477-first-failure-verification.md b/docs/research/2026-09-01-overhaul/477-first-failure-verification.md
new file mode 100644
index 00000000..1ef54fb6
--- /dev/null
+++ b/docs/research/2026-09-01-overhaul/477-first-failure-verification.md
@@ -0,0 +1,132 @@
+# #477 first-failure propagation and extended reproduction
+
+2026-09-05. **First-failure repair verified; extended live run pending.**
+No AMD reset repair or G4 PASS claimed.
+Contract `477-first-failure-contract.md` at `c8ec04e97`, production base
+`cd888a493` (the verified #482 wall fix). FPS remains deferred.
+
+## Lead source checks before implementation
+
+- `VulkanSwapchain.TryAcquire` passes the original result through OnAcquire,
+ then `VulkanGraphicsContext.SwapchainBackbuffer.TryAcquire` accepts only
+ Continue/RecreateAtFrameBoundary. At the base a fatal result becomes false.
+- `VulkanGpuDevice.TryBeginFrame` follows false with SignalTimelineWithoutWork;
+ that later submit can mask the original fatal acquire result. A throw instead
+ propagates before reaching that branch. This is source control-flow evidence,
+ not proof that the owner's actual acquire returned DeviceLost.
+- Present has the same lossy action boundary: the native result becomes Fail,
+ its adapter returns false, and the host treats it as recreation. The fix
+ therefore covers both native-result boundaries using the existing exception.
+- `RenderFrameOrchestrator` opens BeginFrame outside the protected render body;
+ an acquire exception has no frame to close. `GpuDeviceFrameLifetime` publishes
+ CurrentFrame only if BeginFrame returns. `GameWindow.OnRender` catches only
+ VulkanSwapchainOutOfDateException; `GameWindow.Run` retains the original
+ exception in _runFailure and rethrows. None is a new catch/retry policy.
+- The flight controller waits for SubmittedSerial, not the unsubmitted open
+ serial, during WaitForSubmittedWork. No synthetic success or new retirement
+ behavior is introduced by this chunk.
+
+The primary API sources and original confirmed owner failure are linked in the
+contract and `477-cathedral-recurrence.md`. No retail algorithm changes and no
+new intentional retail deviation: the register is not inflated with an API
+error-reporting fix. Independent review must confirm this scope before landing.
+
+## Windows dump access
+
+The owner crash WER record names
+`C:/WINDOWS/LiveKernelReports/WATCHDOG/WATCHDOG-20260905-1641.dmp`.
+Read-only Get-Item and directory enumeration in an escalated tool call both
+returned Windows access denied. Do not claim that the dump is absent, corrupt,
+or analyzed. No permission/ACL change, ownership takeover, driver change, or
+alternative access bypass was attempted. Other in-scope diagnostics continue.
+
+## Extended workload prepared
+
+`tools/overhaul-selfgate/route-477-extended-stability.txt`:
+SHA256 `91563467979063057E875F4431FED6BC253155C36AC0D7D8C6B489B44FABAA8B`.
+Lead checked 403 commands against the existing runner verbs and InputAction
+source, all down/up pairs, and terminal close-client. Declared sleeps total
+1347.75 seconds, plus actual load/capture/checkpoint time. There are 27
+checkpoints and nine PNG requests: saved login; cathedral F4180104, stairwell
+F4180113, hall revisit; then Neftet 8763000E. Cathedral phases each include six
+minutes of dwell with bounded turn/forward/back/camera/combat inputs; Neftet
+adds three jump cycles over three minutes. Exact world movement is not assumed.
+
+Use the latest green reviewed Release, recipe10 package, High/4xMSAA and
+1280x720, without validation/performance/probe overrides. The existing runner
+enables automation checkpoints/capture retention and exact-framebuffer mode;
+therefore this is NOT the identical ordinary owner session. No performance
+comparison or AMD recurrence clearance follows from a clean run alone.
+
+## Frozen implementation and discriminating tests
+
+The single production file adds a fatal-result check after both native calls;
+the existing pure classifier remains unchanged. New tests instantiate the real
+swapchain with a test-only native dispatch table and synthetic handles; they
+do not initialize a real driver or invoke production disposal on those handles.
+Both native argument lists are checked, including acquired semaphore/timeout
+and the per-image present semaphore/index. Successful/suboptimal acquire retains
+the returned index; failures make no index assertion. The uncreated case makes
+zero native calls.
+
+Source SHA256:
+
+- VulkanSwapchain.cs: `D341D3AF7E6590EB800E1024BBEE3277DF4A6D68B9F0D913CEEA0E2E17E9AAA0`.
+- VulkanSwapchainFailureTests.cs: `859B4F6D2A346DB5E226D9871B2FA4D038A08C1EDD6FEE547544EA8BE29F81B1`.
+- Owned-source diff `artifacts/477-first-failure/477-implementation.diff`:
+ `03D4FC0C07C7CECA511CEDA52289C8BB9B1E82DC7AFA9498A6A83A2256F46340`.
+
+Lead independently parsed every TRX below and checked all failed test identities
+and messages: each is the intended Assert.Throws/no-exception failure, never a
+build/load/fixture failure. Results are under `artifacts/477-first-failure/`.
+
+| TRX | Passed / failed | SHA256 |
+|---|---|---|
+|477-prefixed-native-boundary.trx|9 / 6 expected|9091F2E98C236AE22FA4D73BBCB720FDB386D1CD4245076A47DBEA83878E1BAE|
+|477-fixed-focused.trx|82 / 0|7080D613B0A98BCB2766015712FDE37288E8E8F170ED5CF3A6C0026D974EB626|
+|477-sabotage-acquire.trx|12 / 3 expected acquire failures|CE17F772B7908A1C6B5362EF0F5B67BBA60863D47E3C6A6A39DFDCC64236931B|
+|477-sabotage-present.trx|12 / 3 expected present failures|90829EFFA8208A03851EED6D9090106613EF1845C103AF79F30507B9BB9B45D0|
+|477-restored-focused.trx|82 / 0|96950E045213534FFA6583DBECECD9ECFC6F7F04511E81568AA494386ED2BA7E|
+
+The fatal cases are DeviceLost, SurfaceLost and an unknown negative result at
+both boundaries. The 82-test focused set also includes existing swapchain
+configuration, frame-flight, graphics acquisition, synchronization-dependency,
+and backbuffer-capture validity tests. No skips or failed builds. Implementer
+Release is 0 warnings/0 errors, execution ownership returned to the lead.
+No implementation fix round was required; deliberate sabotage is not a repair
+round. Native-result injection is not a real GPU reset or a teardown-recovery
+test. The source analysis above supplies the unchanged downstream-call context.
+
+## Independent review and integrated gate
+
+Wegener's one independent production/API review is PASS with no findings:
+both frozen hashes, native callback arguments, actual exception propagation,
+unchanged normal policy/retirement, and the exact failed test identities were
+verified read-only. No retail port was introduced. The lead independently
+checked those source claims. The implementer's build log omitted its final
+exit summary; the lead's integrated gate below supplies explicit build/process
+outcomes, rather than treating absence of log errors as proof.
+
+`artifacts/477-first-failure-integrated/release-gate-summary.json`:
+locked restore, Release **0 warnings/0 errors**, **17,051 passed / 0 failed /
+0 skipped**, 14 assemblies, exit0, no retry. Literal filter unchanged from the
+previous wall gate:
+`Lane!=InstalledDat&Lane!=Linux&Lane!=Manual&Lane!=Timing&Lane!=Live&Lane!=PreparedPackage`.
+Lead rehashed all **32/32** manifest entries. Summary SHA256
+`AE0CD11D8590C68A65CC8E7CCDF618AF8430B52C5B8C3F73B401D6020ACEBBF3`.
+Build/test18:54:23–18:55:51+02, both clients absent. This is the integrated
+hermetic correctness lane, not a fresh installed-DAT or graphical gate.
+
+Exact candidate DLLs under `src/AcDream.App/bin/Release/net10.0/`:
+
+- App `BE4EFB7A7E2A8B1BF6002BAC11D52F712D77A86EA6492B9815E89546B9ED9775`.
+- Core `4CC5B32751E2F75805FEBA0F91E9808449570C83842C95556ACE3516B5D1E317`.
+
+Built at c8ec04e97 plus the frozen scoped source delta; recorded dirty state
+is truthful. Core source did not change, but its rebuilt DLL identity differs
+from the earlier #482 gate; do not claim byte-identical binaries. The next
+launch must use these hashes without rebuilding after graphics starts.
+
+Graphical run results remain pending. Both clients were absent and ACE
+UDP9000/PID13340 was up before dispatch; recheck before launch. Architecture
+documents the error boundary. No new intentional retail deviation was added.
diff --git a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
index f775101f..041421a3 100644
--- a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
+++ b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
@@ -5106,3 +5106,18 @@ Nine PNGs opened, both clients exit0/gracefully, checkpoints completed/visible/
cuefalse/invariants0/transit idle. No clients/debugger remain, ACE up. Owner
wall acceptance remains pending; the short runs do not clear stability.
AP159 ramp, AMD#477 and G4 stay open, FPS remains deferred.
+
+## 63. #477 first-failure result preservation — 2026-09-05
+
+Contractc8ec04e97 addresses the actual lossy native acquire/present boundary,
+not an inferred AMD cause. Fatal results now raise the existing exception with
+original operation/result before the adapter reduces them to false. Normal
+unavailable/recreation outcomes and all synchronization/retirement stay intact.
+One production file plus native-dispatch tests; six pre-fix failures, separate
+acquire/present sabotages three each, restored82/82. One independent API/
+production review PASS, lead source verification, integrated locked Release
+0W0E/17051pass/0fail/0skip, manifest32/32. No new intentional retail deviation.
+`477-first-failure-verification.md` records frozen hashes and limits. Extended
+cathedral/Neftet reproduction next; its automation is not the exact owner
+session. Windows denied watchdog-dump access; no bypass/settings change.
+#477/AP159/G4 remain open, owner wall acceptance pending, FPS deferred.
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs
index af3d0238..aa94a113 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs
@@ -15,9 +15,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// , which are pure and unit-tested.
/// What is left here is the calls themselves.
///
-/// Untested by the implementing slice. Nothing below can run
-/// without a window and a driver, so it carries no automated coverage at V5. The
-/// manual "Vulkan boots to a clear colour" check is the gate.
+/// Acquire and present result handling has automated coverage through
+/// controlled native responses. Real window/driver creation and presentation
+/// still require the manual Vulkan gate.
///
internal sealed unsafe class VulkanSwapchain : IDisposable
{
@@ -265,7 +265,10 @@ internal sealed unsafe class VulkanSwapchain : IDisposable
acquired,
default,
ref imageIndex);
- return VulkanSwapchainRecreationPolicy.OnAcquire(result);
+ VulkanSwapchainAction action = VulkanSwapchainRecreationPolicy.OnAcquire(result);
+ if (action == VulkanSwapchainAction.Fail)
+ throw new VulkanCallException("vkAcquireNextImageKHR", result);
+ return action;
}
/// Present the acquired image, waiting on that image's render-complete semaphore.
@@ -284,7 +287,10 @@ internal sealed unsafe class VulkanSwapchain : IDisposable
PImageIndices = &index,
};
Result result = _swapchainApi.QueuePresent(presentQueue, &present);
- return VulkanSwapchainRecreationPolicy.OnPresent(result);
+ VulkanSwapchainAction action = VulkanSwapchainRecreationPolicy.OnPresent(result);
+ if (action == VulkanSwapchainAction.Fail)
+ throw new VulkanCallException("vkQueuePresentKHR", result);
+ return action;
}
///
diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanSwapchainFailureTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanSwapchainFailureTests.cs
new file mode 100644
index 00000000..ba69c88e
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanSwapchainFailureTests.cs
@@ -0,0 +1,241 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using AcDream.App.Rendering.Gpu.Vk;
+using Silk.NET.Core.Contexts;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.KHR;
+using VkSemaphore = Silk.NET.Vulkan.Semaphore;
+
+namespace AcDream.App.Tests.Rendering.Gpu.Vk;
+
+///
+/// #477: exercise the real swapchain methods through Silk's native entry
+/// points. Fatal results must retain their first operation and exact VkResult;
+/// usable, unavailable, and recreation results retain the existing policy.
+///
+public sealed unsafe class VulkanSwapchainFailureTests
+{
+ [Theory]
+ [InlineData(Result.ErrorDeviceLost)]
+ [InlineData(Result.ErrorSurfaceLostKhr)]
+ [InlineData((Result)(-123456789))]
+ public void AcquireFatalResult_ThrowsOriginalNativeFailure(Result result)
+ {
+ using var fixture = new Fixture();
+ fixture.Native.AcquireResult = result;
+
+ VulkanCallException error = Assert.Throws(() =>
+ fixture.Swapchain.TryAcquire(Acquired, Timeout, out _));
+
+ Assert.Equal("vkAcquireNextImageKHR", error.Operation);
+ Assert.Equal(result, error.Result);
+ AssertAcquireCall(fixture.Native);
+ Assert.Empty(fixture.Native.Presents);
+ }
+
+ [Theory]
+ [InlineData(Result.ErrorDeviceLost)]
+ [InlineData(Result.ErrorSurfaceLostKhr)]
+ [InlineData((Result)(-123456789))]
+ public void PresentFatalResult_ThrowsOriginalNativeFailure(Result result)
+ {
+ using var fixture = new Fixture();
+ fixture.Native.PresentResult = result;
+
+ VulkanCallException error = Assert.Throws(() =>
+ fixture.Swapchain.Present(PresentQueue, NativeImageIndex));
+
+ Assert.Equal("vkQueuePresentKHR", error.Operation);
+ Assert.Equal(result, error.Result);
+ AssertPresentCall(fixture.Native);
+ Assert.Empty(fixture.Native.Acquisitions);
+ }
+
+ [Theory]
+ [InlineData(Result.Success, (int)VulkanSwapchainAction.Continue)]
+ [InlineData(Result.SuboptimalKhr, (int)VulkanSwapchainAction.RecreateAtFrameBoundary)]
+ [InlineData(Result.Timeout, (int)VulkanSwapchainAction.Idle)]
+ [InlineData(Result.NotReady, (int)VulkanSwapchainAction.Idle)]
+ [InlineData(Result.ErrorOutOfDateKhr, (int)VulkanSwapchainAction.RecreateNow)]
+ public void AcquireNonfatalResult_PreservesActionAndUsableImageIndex(Result result, int action)
+ {
+ using var fixture = new Fixture();
+ fixture.Native.AcquireResult = result;
+
+ Assert.Equal((VulkanSwapchainAction)action,
+ fixture.Swapchain.TryAcquire(Acquired, Timeout, out uint imageIndex));
+
+ AssertAcquireCall(fixture.Native);
+ if (result is Result.Success or Result.SuboptimalKhr)
+ Assert.Equal(NativeImageIndex, imageIndex);
+ }
+
+ [Theory]
+ [InlineData(Result.Success, (int)VulkanSwapchainAction.Continue)]
+ [InlineData(Result.SuboptimalKhr, (int)VulkanSwapchainAction.RecreateAtFrameBoundary)]
+ [InlineData(Result.ErrorOutOfDateKhr, (int)VulkanSwapchainAction.RecreateNow)]
+ public void PresentNonfatalResult_PreservesAction(Result result, int action)
+ {
+ using var fixture = new Fixture();
+ fixture.Native.PresentResult = result;
+
+ Assert.Equal((VulkanSwapchainAction)action,
+ fixture.Swapchain.Present(PresentQueue, NativeImageIndex));
+
+ AssertPresentCall(fixture.Native);
+ }
+
+ [Fact]
+ public void UncreatedSwapchain_ReturnsRecreateWithoutNativeAcquire()
+ {
+ using var fixture = new Fixture(created: false);
+ fixture.Native.AcquireResult = Result.ErrorDeviceLost;
+
+ Assert.False(fixture.Swapchain.IsCreated);
+ Assert.Equal(VulkanSwapchainAction.RecreateNow,
+ fixture.Swapchain.TryAcquire(Acquired, Timeout, out _));
+ Assert.Empty(fixture.Native.Acquisitions);
+ Assert.Empty(fixture.Native.Presents);
+ }
+
+ private static readonly Device Device = new((nint)0x4771);
+ private static readonly SwapchainKHR Handle = new(0x4772ul);
+ private static readonly VkSemaphore Acquired = new(0x4773ul);
+ private static readonly Queue PresentQueue = new((nint)0x4774);
+ private static readonly VkSemaphore RenderComplete = new(0x4775ul);
+ private const ulong Timeout = 1_234_567ul;
+ private const uint NativeImageIndex = 2u;
+
+ private static void AssertAcquireCall(RecordingNativeContext native)
+ {
+ AcquireCall call = Assert.Single(native.Acquisitions);
+ Assert.Equal(Device, call.Device);
+ Assert.Equal(Handle, call.Swapchain);
+ Assert.Equal(Timeout, call.Timeout);
+ Assert.Equal(Acquired, call.Semaphore);
+ Assert.Equal(default, call.Fence);
+ }
+
+ private static void AssertPresentCall(RecordingNativeContext native)
+ {
+ PresentCall call = Assert.Single(native.Presents);
+ Assert.Equal(PresentQueue, call.Queue);
+ Assert.Equal(StructureType.PresentInfoKhr, call.Type);
+ Assert.Equal(1u, call.WaitCount);
+ Assert.Equal(RenderComplete, call.Wait);
+ Assert.Equal(1u, call.SwapchainCount);
+ Assert.Equal(Handle, call.Swapchain);
+ Assert.Equal(NativeImageIndex, call.ImageIndex);
+ }
+
+ private sealed class Fixture : IDisposable
+ {
+ internal RecordingNativeContext Native { get; } = new();
+ private readonly Silk.NET.Vulkan.Vk _vk;
+ private readonly KhrSurface _surface;
+ private readonly KhrSwapchain _swapchainApi;
+ internal VulkanSwapchain Swapchain { get; }
+
+ internal Fixture(bool created = true)
+ {
+ _vk = new Silk.NET.Vulkan.Vk(Native);
+ _surface = new KhrSurface(Native);
+ _swapchainApi = new KhrSwapchain(Native);
+ Swapchain = new VulkanSwapchain(_vk, _surface, _swapchainApi,
+ new PhysicalDevice((nint)0x4776), Device, new SurfaceKHR(0x4777ul),
+ new VulkanQueueFamilyChoice(0, 0));
+ // Install synthetic native handles only. This fixture never creates
+ // a driver resource and calls the production acquire/present bodies.
+ SetField(Swapchain, "_swapchain", created ? Handle : default(SwapchainKHR));
+ SetField(Swapchain, "_renderComplete", new VkSemaphore[]
+ {
+ new(0x4778ul), new(0x4779ul), RenderComplete,
+ });
+ }
+
+ public void Dispose()
+ {
+ // No VulkanSwapchain.Dispose: its synthetic handles own no native
+ // resources. Dispose the Silk dispatch tables and test context.
+ _swapchainApi.Dispose();
+ _surface.Dispose();
+ _vk.Dispose();
+ Native.Dispose();
+ }
+
+ private static void SetField(object target, string name, object value) =>
+ (target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException($"Missing {name}."))
+ .SetValue(target, value);
+ }
+
+ private readonly record struct AcquireCall(
+ Device Device, SwapchainKHR Swapchain, ulong Timeout, VkSemaphore Semaphore, Fence Fence);
+ private readonly record struct PresentCall(
+ Queue Queue, StructureType Type, uint WaitCount, VkSemaphore Wait,
+ uint SwapchainCount, SwapchainKHR Swapchain, uint ImageIndex);
+
+ private sealed class RecordingNativeContext : INativeContext
+ {
+ private static RecordingNativeContext? s_active;
+ internal Result AcquireResult { get; set; } = Result.Success;
+ internal Result PresentResult { get; set; } = Result.Success;
+ internal List Acquisitions { get; } = [];
+ internal List Presents { get; } = [];
+
+ internal RecordingNativeContext()
+ {
+ Assert.Null(s_active);
+ s_active = this;
+ }
+
+ public nint GetProcAddress(string proc, int? slot = null) => proc switch
+ {
+ "vkAcquireNextImageKHR" =>
+ (nint)(delegate* unmanaged)
+ &AcquireNextImage,
+ "vkQueuePresentKHR" =>
+ (nint)(delegate* unmanaged)&QueuePresent,
+ _ => (nint)(delegate* unmanaged)&NoOp,
+ };
+
+ public bool TryGetProcAddress(string proc, out nint addr, int? slot = null)
+ {
+ addr = GetProcAddress(proc, slot);
+ return true;
+ }
+
+ public void Dispose()
+ {
+ if (ReferenceEquals(s_active, this))
+ s_active = null;
+ }
+
+ [UnmanagedCallersOnly]
+ private static Result AcquireNextImage(
+ Device device, SwapchainKHR swapchain, ulong timeout,
+ VkSemaphore semaphore, Fence fence, uint* imageIndex)
+ {
+ RecordingNativeContext active = s_active!;
+ active.Acquisitions.Add(new AcquireCall(device, swapchain, timeout, semaphore, fence));
+ if (active.AcquireResult is Result.Success or Result.SuboptimalKhr)
+ *imageIndex = NativeImageIndex;
+ return active.AcquireResult;
+ }
+
+ [UnmanagedCallersOnly]
+ private static Result QueuePresent(Queue queue, PresentInfoKHR* present)
+ {
+ RecordingNativeContext active = s_active!;
+ active.Presents.Add(new PresentCall(queue, present->SType,
+ present->WaitSemaphoreCount, present->PWaitSemaphores[0],
+ present->SwapchainCount, present->PSwapchains[0], present->PImageIndices[0]));
+ return active.PresentResult;
+ }
+
+ [UnmanagedCallersOnly]
+ private static void NoOp()
+ {
+ }
+ }
+}