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:
Erik 2026-09-05 18:57:08 +02:00
parent c8ec04e97d
commit f4da814af6
7 changed files with 428 additions and 6 deletions

View file

@ -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,

View file

@ -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)

View file

@ -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;

View file

@ -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:2318: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.

View file

@ -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.

View file

@ -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>

View file

@ -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()
{
}
}
}