Skip to content

perf(render): steady-state GPU scales with WINDOW area when only the picture changes #131

Description

@relmer

Casso's steady-state GPU cost scales with window area even when the only
thing changing is the emulated picture, which occupies a small fraction of it.
Three things repaint the whole window every frame, unconditionally, and none of
them need to.

This issue is the investigation, not a committed design. Step 1 is a
measurement
, and it decides which of the two candidate directions is worth
doing — possibly neither.

Where it stands today

Measured on desk-scene-models at commit 918b93c0, RELMER-SP8, Debug,
at a BASIC prompt with a blinking cursor (so it presents every frame):

client area GPU
1400x1470 (2.06 Mpix) 10.2%
2880x1837 (5.29 Mpix) 18.9%

Roughly 4.7% fixed + 2.7% per Mpix of window area. For comparison, before
the CRT chain was sized to the picture (21f7ff16) it was ~8% fixed +
~8%/Mpix, so the area coefficient is already about a third of what it was.

Per frame, in that state, the GPU does:

  • The 3D desk scene: three draws. Two full-back-buffer textured quads (the
    cached back and front plates) and the picture mesh between them. No model
    rasterization, no MSAA resolve, no shadow passes, no vertex uploads. The
    plate cache was instrumented and hits 60 frames out of 60 -- the scene is
    genuinely idle, which is why turning the 3D CRT monitor off in Settings
    changes nothing measurable.
  • The CRT chain: 2 to 9 fullscreen passes at PICTURE size, plus one
    CopyResource of the picture-sized texture for the persistence prior.
    Brightness and the final copy always run; bloom (3 passes), color bleed,
    scanlines and gamma each skip when their parameter is zero.
  • Chrome: a full Dxui panel-tree repaint, uncached, every frame.

At 2880x1837 the two plate composites alone are ~10.6 Mpix of blending per
frame, 635 Mpix/s, to re-lay furniture that is bit-identical to the previous
frame. The picture is a small hole in the middle of them.

What blocks partial redraw

Three unconditional full-window operations:

  1. DxuiRenderTarget.cpp:288 clears the entire back buffer.
  2. DeskScene::Render composites the back and front plates over the whole
    back buffer (CompositeFullTarget).
  3. DxuiHwndSource::PaintContent calls RootPanel()->Paint, which walks the
    entire panel tree. Dxui has no invalidation model.

And the swap chain is DXGI_SWAP_EFFECT_FLIP_DISCARD
(DxuiHwndSource.cpp:1418), which discards back-buffer contents -- which is
why dirty-rect Present1 was previously found inert here.

Step 1 (do this first): where do the ~14 points actually go?

At 5.29 Mpix the area term contributes ~14 of the 18.9 points. That is split
between the two plate composites, the chrome repaint, and the present /
DirectComposition composite, and the split is unknown. Get it before
committing to either direction below -- if chrome dominates, option B is the
answer and the swap-chain work is the wrong target.

Suggested instrument: D3D11 timestamp queries around each stage, or simply
short-circuit one stage at a time in a scratch build and re-measure back to
back in one sitting.

Measure in Release. The numbers above are Debug and are useful only as
ratios. And never A/B GPU cost across separate builds -- this iGPU's clocks
move enough between runs to invent effects that do not exist; switch the thing
at runtime, or short-circuit and re-measure in one session.

Option A: real dirty-rect presentation

Switch to DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, then scissor the clear, the
composites, and the chrome paint to the changed region.

  • The swap-effect change itself is one line and buys nothing on its own.
    It only removes the prohibition on partial redraw; the partial redraw is the
    work.
  • FLIP_SEQUENTIAL does not hand you the previous frame. With
    BufferCount = 2 the buffer you acquire holds the frame from two presents
    ago, so this needs per-buffer dirty tracking (union the dirty regions over
    the last N frames) or alternating frames show stale content.
  • Scissoring the clear and the two composites is contained -- they are already
    textured quads.
  • Scissoring the chrome is the Dxui-wide invalidation refactor: panels
    track dirty rects, the painter clips, and every widget that animates has to
    report invalidation correctly. This is the bulk of the work and nearly all
    of the risk; a missed invalidation is a visual artifact that surfaces in one
    interaction weeks later. Note this overlaps Move Dxui compositing off the UI thread (retained-mode immutable-snapshot) #100, which wants a retained-mode
    immutable-snapshot model for a different reason (getting paint off the UI
    thread) -- if Move Dxui compositing off the UI thread (retained-mode immutable-snapshot) #100 happens, its snapshot model is the natural place for
    invalidation to live, and these should be planned together rather than
    twice.

Option B: a composed-frame cache (no Dxui refactor)

Keep a persistent full-size "composed frame" texture. Render the picture into
it scissored, and copy it to the back buffer. Two full-screen blends plus a
full chrome repaint become one full-screen copy plus a small blend.

This needs only a coarse global "chrome changed" flag -- any input event,
tooltip tick, animation or state change sets it, otherwise the previous
composition is reused -- not per-panel regions. Still O(window area), but a
copy is much cheaper than blend-plus-repaint.

Lower ceiling than A, far lower cost and risk. Probably the better
effort/reward if step 1 says chrome and composites are the bulk.

What will not go away

Whatever is done here, the floor is set by:

  • DWM / DirectComposition compositing the window itself. This is a composition
    swap chain (CreateSwapChainForComposition), and that composite is not ours
    to scissor.
  • The CRT chain running its passes at picture size.
  • Re-rendering the picture region itself when it genuinely changes.

So "zero while the picture animates" is not on the table; the goal is to make
the cost scale with the PICTURE's area rather than the WINDOW's.

Prior art in the tree

  • 21f7ff16 -- sized the CRT chain to the picture instead of the back buffer
    (64% -> 24.5% at the time).
  • 44972523 -- cached the desk scene into the two plates (41.6% -> 15.8%).
  • 918b93c0 -- stopped presenting 60 fps over a static picture; idle went
    47% -> 0%. That fixed the IDLE case. This issue is the ACTIVE case, where
    the picture is legitimately changing and the window is repainted around it.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestimpact: engineeringInternal only: testability, architecture, maintainabilitypriority: mediumReal value, niche or polish (end-user-value triage)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions