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.
This commit is contained in:
parent
c8ec04e97d
commit
f4da814af6
7 changed files with 428 additions and 6 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
|||
/// <see cref="VulkanSwapchainRecreationPolicy"/>, which are pure and unit-tested.
|
||||
/// What is left here is the calls themselves.
|
||||
///
|
||||
/// <para><b>Untested by the implementing slice.</b> 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.</para>
|
||||
/// <para>Acquire and present result handling has automated coverage through
|
||||
/// controlled native responses. Real window/driver creation and presentation
|
||||
/// still require the manual Vulkan gate.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Present the acquired image, waiting on that image's render-complete semaphore.</summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// #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.
|
||||
/// </summary>
|
||||
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<VulkanCallException>(() =>
|
||||
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<VulkanCallException>(() =>
|
||||
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<AcquireCall> Acquisitions { get; } = [];
|
||||
internal List<PresentCall> 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<Device, SwapchainKHR, ulong, VkSemaphore, Fence, uint*, Result>)
|
||||
&AcquireNextImage,
|
||||
"vkQueuePresentKHR" =>
|
||||
(nint)(delegate* unmanaged<Queue, PresentInfoKHR*, Result>)&QueuePresent,
|
||||
_ => (nint)(delegate* unmanaged<void>)&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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue