SteonMod
View as Markdown

0001: Composite guest frames by GPU-copying them into Godot-owned textures

  • Date: 2026-10-02
  • Status: accepted (step 0.9)

Context

Guests render into shared D3D12 textures (color + linear depth). Fusion (Godot 4.7.2, D3D12) has to draw them inside its scene. The plan was to wrap each shared texture with RenderingDevice.texture_create_from_extension and sample it directly through a Texture2DRD.

What we found:

  • texture_create_from_extension works on D3D12. It takes an ID3D12Resource* and doesn't add a reference, so we have to keep the resource alive ourselves.
  • Texture2DRD always creates a shared view of the texture it wraps (TextureStorage::texture_rd_initialize).
  • In debug and editor builds, Godot's D3D12 driver refuses views of textures it didn't allocate (ERR_FAIL_COND_V(!owner_tex_info->owner_info.allocation) inside #ifdef DEBUG_ENABLED). The rest of that function would work; the check is only a sanity check. The material then silently falls back to a white texture, which covered the whole screen in our first test.
  • Godot does expose its main D3D12 command queue: get_driver_resource(DRIVER_RESOURCE_COMMAND_QUEUE, RID(), 0).

Decision

Each frame, when a new guest frame is ready, Fusion GPU-copies it with RenderingDevice.texture_copy from the shared texture (wrapped with texture_create_from_extension, usage CAN_COPY_FROM) into two textures Godot created itself. The compositing material samples those through Texture2DRD.

After each frame (frame_post_draw), Fusion signals its own fence on Godot's queue with the frame number. A guest buffer goes back to the guest once that fence passes the frame whose copy used it.

Consequences

  • Works in the editor and in exported games; no Godot patch needed.
  • Costs one GPU copy of color + depth per new guest frame. At 1600×900 on an RTX 3050 Laptop it was below measurement noise (about ±0.5 ms).
  • Guest buffers return after about one frame, instead of staying held while shown.
  • Godot's render graph orders the copy and issues its barriers.
  • Pitfall found while testing: Godot skips drawing (and so doesn't send recorded GPU commands) while its window can't be drawn, e.g. minimized or screen off. A fence signaled at shutdown can therefore pass before recorded copies are submitted. Releasing the guest's D3D12 objects then crashed Godot on exit (0xC0000005). Fix: release D3D12 objects only after Godot has drawn a few more frames (the DEFERRED list in fusion-godot/src/guest_view.rs); anything left at exit is freed by Windows.
  • If Godot ever allows views of external textures in debug builds, direct sampling (zero-copy) becomes possible. The copy is cheap enough that this isn't a priority.

Verification

godot/scenes/tests/compositor_test.tscn with -- --auto:

  • freezes the guest;
  • draws Godot's own cube at the pose the guest reports;
  • compares the outlines: 100% overlap in 4 of 4 runs, including where Godot boxes hide parts of the cube.