Skip to content

Editor tests on Direct3D11, Direct3D12 and Vulkan, with debug devices - #3473

Merged
xen2 merged 9 commits into
stride3d:masterfrom
xen2:feature/editor-tests-vulkan
Sep 23, 2026
Merged

xen2 merged 9 commits into
stride3d:masterfrom
xen2:feature/editor-tests-vulkan

Conversation

@xen2

@xen2 xen2 commented Sep 23, 2026

Copy link
Copy Markdown
Member

PR Details

test-windows-editor.yml runs the editor fixtures once per graphics API (a manual run can pick one). The Windows Vulkan setup (SDK, validation layer, Lavapipe ICD) becomes a composite action shared with the game and enduser workflows. Game Studio runs with /DebugEditorGraphics, so the validation layers are on and their messages show in the test output. Vulkan synchronization validation stays off in this job: on the 4-core Lavapipe runner it starves the asset build and the fixtures time out (the game tests and the samples run it).

Running the editor on the other APIs found five engine bugs, fixed here:

  • Thumbnails leaked GPU memory (Direct3D12, Vulkan). ThumbnailGenerator never called GraphicsDevice.Begin()/End(), so the deferred resources of each thumbnail were never released. On the CI runner one project load ran out of memory and lost the device.
  • Dangling shader entry point name (Vulkan). PipelineState kept a pointer into a managed array after its fixed block; the pipeline was created later. Any Vulkan user could hit "Entry point not found".
  • Swap chain resized while the GPU still drew (Direct3D12). ResizeBuffers failed, the whole device was re-created, Game Studio crashed later.
  • Live-object report through every device's callback (Direct3D12). D3D12 devices are singletons per adapter; each Dispose() reported the objects of all GraphicsDevice instances, through all their callbacks.
  • Fail-fast at exit with the debug layer (Direct3D12). A never-disposed debug device kept its message callback; the debug layer called it after the runtime shut down.

Also: a lost device is logged on any device, a failed re-creation no longer dereferences a null device, GraphicsDeviceManager logs the exceptions it swallowed, and the harness waits for Game Studio's initial editors before it drains the queues.

Verification

All three editor jobs green, 5/5 each, no validation message: https://github.com/xen2/stride/actions/runs/35725256240 (before the last review fixes and the shared action; the PR checks cover the final commits). The Direct3D11 job with the debug layer is not slower than before.

The test host gets the same per-API assembly layout as Game Studio and picks the
API from STRIDE_GRAPHICS_API at startup. The workflow runs the fixtures once per
API (Direct3D11, Direct3D12, Vulkan); the Vulkan job uses Lavapipe with the
validation layers, without synchronization validation: on the 4-core runner it
starves the asset build and the fixtures time out. The games the fixtures build
stay on Direct3D11. A manual run can pick one API.

The Windows Vulkan setup (SDK, validation layer, Lavapipe ICD) becomes a
composite action shared with the game and enduser workflows.
… crashes

Direct3D12 logged the removal reason only on a debug device, and polling the
status after a failed re-creation dereferenced the null native device.
GraphicsDeviceManager swallowed the exceptions of a failed presenter resize
and of a failed re-creation. The editor test host writes the warnings and
errors of the global logger to its diag log and to stderr.
The thumbnail generator drew without GraphicsDevice.Begin()/End(). On Direct3D12
and Vulkan, End() advances the frame fence and releases the deferred resources
of the frame (discarded dynamic buffers, upload buffers), so every thumbnail
leaked them until the device was disposed. With WARP on a CI runner, one project
load was enough to run out of memory and lose the device.

The status overlay helper had the same pattern, and created a GraphicsContext
(with its command list) per use without disposing it.
…ts live objects

D3D12 devices are singletons per adapter, so every GraphicsDevice of the
process shares the native device, its info queue and its live objects. Each
Dispose() reported the objects of all of them, through the callbacks of all of
them: 4 devices in Game Studio gave 4 copies of a 80k-line report per Dispose.
A debug device that is never disposed keeps its callback until the process
ends, and the debug layer still emits messages from its own thread while it
tears down. Managed code on a thread the runtime already left is a fail-fast
(0xC0000602), seen at the end of every Game Studio session with
/DebugEditorGraphics on Direct3D12.
…ine is created

CreateShaderStages stored a pointer into the managed entry point array, pinned
only while the stage info was filled. The pipeline was created later, so the
name was garbage whenever the GC had moved the array in between: Lavapipe
then failed with "Entry point not found for fragment shader". The names are
now copied to native memory for the duration of the creation.
…s resized

ResizeBuffers failed with "referenced by GPU operations in-flight" when the
window was resized while a frame was still executing. GraphicsDeviceManager
then re-created the whole device, and Game Studio crashed later.
…the queues

Game Studio opens the default scene after its window is loaded. A fixture that
called WaitIdle right after the window appeared found empty queues and no
editor game, and took its screenshot while the scene was still loading.
GameStudioWindow exposes that startup task, the harness awaits it first.
@xen2 xen2 added ci-enduser Run the end-user sample screenshot + packaging suite on this PR ci-editor Run the GameStudio editor screenshot suite on this PR labels Sep 23, 2026
@xen2
xen2 merged commit ecf7811 into stride3d:master Sep 23, 2026
45 checks passed
@xen2
xen2 deleted the feature/editor-tests-vulkan branch September 23, 2026 08:20
var depthStencil = GraphicsContext.Allocator.GetTemporaryTexture2D(request.Size.X, request.Size.Y, PixelFormat.D24_UNorm_S8_UInt, TextureFlags.DepthStencil);

// A thumbnail is a frame: without End() the device never releases what a frame leaves behind
GraphicsDevice.Begin();

@Kryptos-FR Kryptos-FR Sep 23, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could think of having an extension method that returns a IDisposable so we can do something like:

using var scope = GraphicsDevice.BeginScope();

Though we also need to make sure it doesn't allocate or box a value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-editor Run the GameStudio editor screenshot suite on this PR ci-enduser Run the end-user sample screenshot + packaging suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants