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