From f4da814af6971b3394186c3e5ec3c04831fc1df2 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 5 Sep 2026 18:57:08 +0200 Subject: [PATCH] fix #477: preserve fatal swapchain operation and result Native-boundary injection: old code 9 pass / 6 expected Assert.Throws failures. Separate acquire and present sabotage each fail their 3 fatal cases; restored focused gate 82/82. One independent API/production review PASS. Lead locked Release 0W0E; literal-hermetic 17051/0/0, manifest32/32. No retirement, recovery, normal result policy or retail behavior changes. This is first-failure evidence preservation, not an AMD reset fix; extended reproduction and G4 remain open. --- docs/ISSUES.md | 13 +- docs/architecture/acdream-architecture.md | 7 + ...-09-01-campaign-overhaul-world-solidity.md | 10 + .../477-first-failure-verification.md | 132 ++++++++++ .../s5-consumers-material-closeout-packet.md | 15 ++ .../Rendering/Gpu/Vk/VulkanSwapchain.cs | 16 +- .../Gpu/Vk/VulkanSwapchainFailureTests.cs | 241 ++++++++++++++++++ 7 files changed, 428 insertions(+), 6 deletions(-) create mode 100644 docs/research/2026-09-01-overhaul/477-first-failure-verification.md create mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanSwapchainFailureTests.cs 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() + { + } + } +}