Skip to content

fix(rtg): restore dirty-page tracking for RTG display updates - #44

Merged
midwan merged 8 commits into
masterfrom
fix/27-rtg-mode-switch-dirty-pages
Sep 16, 2026
Merged

midwan merged 8 commits into
masterfrom
fix/27-rtg-mode-switch-dirty-pages

Conversation

@midwan

@midwan midwan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #27

Root cause

amiberry-lite 5.9.0 ported the RTG dirty-page refresh optimization from the main Amiberry repo, but two of its follow-up fixes were missed, which breaks RTG display updates entirely on non-Windows builds:

  1. dirty_page_map was never allocated. picasso_allocatewritewatch() allocated gwwbuf but never the map, so picasso_getwritewatch() always returned -1 and partial flushes copied zero pages — and mark_dirty() from the P96 trap handlers was a no-op. RTG output only updated when something forced a full refresh (mode-switch instant, palette change, GUI open/close).
  2. Plain CPU stores to VRAM never marked pages dirty. Workbench renders RTG screens largely through direct CPU writes (not P96 API calls), so even with the map allocated those writes stayed invisible to the flush path.

Net effect, exactly as reported in #27: after switching native→RTG the screen stays grey/blank, and after RTG→native the last RTG image stays frozen — until F12 forces picasso_refresh() (a full refresh), which is why opening and dismissing the GUI "fixes" it.

Changes (ported from the main Amiberry repository, adapted to lite)

Change Source
Allocate dirty_page_map in picasso_allocatewritewatch() + min/max dirty-page bounds so picasso_getwritewatch() only scans the touched range amiberry 875c4198
Custom gfxmem bank put handlers (lput/wput/bput) calling mark_dirty() after every store; banks flagged S_WRITE so JIT-routed writes also pass through the handlers amiberry 63a74239
Clamp unlockscr dirty rectangle to the current surface so a stale width_allocated after a mode switch can't feed SDL_UpdateTexture an out-of-bounds region amiberry 756ebf1e

Verification

Tested on macOS (SDL2 build) with an A4000/KS 3.1 + Workbench 3.1 + Picasso96 3.6.3 install and a Zorro III UAEgfx board, using the screen test program attached to issue #27:

  • native→RTG: boot into native, screen -m 1024x768 opens the RTG screen — display switches and shows the test pattern immediately; closing it (LMB) returns to the RTG Workbench. ✅
  • RTG→native: boot into RTG Workbench, then open a native chipset screen — display switches to it immediately, no F12 needed. ✅
  • Workbench rendering on the RTG screen (window dragging, icons) keeps updating continuously.

Switching between RTG and native modes left the display stale until the
GUI was opened (fixes #27).

The RTG dirty-page refresh optimization added in 5.9.0 (ported from
Amiberry) only ever flushed full refreshes on non-Windows builds:

- dirty_page_map was never allocated, so picasso_getwritewatch() always
  returned -1 and partial flushes copied zero pages; mark_dirty() from
  the P96 trap handlers was a no-op.
- plain CPU stores to VRAM never marked pages dirty; only P96 API ops
  (FillRect, BlitRect, ...) did, and even those were lost to the
  unallocated map. Workbench renders mostly via direct CPU writes, so
  after a mode switch the RTG screen stayed grey/frozen until something
  forced a full refresh (e.g. opening and dismissing the GUI).

Port the follow-up fixes from the main Amiberry repository:

- allocate dirty_page_map in picasso_allocatewritewatch and track
  min/max dirty page bounds so picasso_getwritewatch scans only the
  touched range (amiberry 875c4198)
- give the gfxmem banks custom put handlers (lput/wput/bput) that call
  mark_dirty() after every store, and flag the banks with S_WRITE so
  JIT-routed writes also go through the handlers (amiberry 63a74239)
- clamp the unlockscr dirty rectangle to the current surface so a stale
  width_allocated after a mode switch cannot feed SDL_UpdateTexture an
  out-of-bounds region (amiberry 756ebf1e)
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T21:32:43.554525Z 942eeed Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ea3470cef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/osdep/picasso96.cpp
Comment thread src/osdep/picasso96.cpp Outdated
- picasso_getwritewatch(): with VGA screen splitting active the getter
  was called once per source region but drained the whole dirty map on
  the first call, so pages belonging to the wrapped lower region were
  dropped by the first pass's range filter and missing from the second.
  Drain once per frame and reuse the page list for both regions.
- picasso_getwritewatch(): return the region base as board base plus
  the screen offset, matching the Windows GetWriteWatch semantics.
  Returning the bare board base let the caller's range filter accept
  pages below the visible screen (offscreen bitmaps, split region).
- mark_dirty()/picasso_getwritewatch(): synchronize the dirty bounds
  with the RTG render thread. With gfxcard_multithread enabled,
  mark_dirty() runs from the CPU write handlers while render_thread()
  drains the map, racing on the bounds (and allowing a drain to reset
  the bounds between a writer's compare and its bit-set, losing the
  page until another write covers it). The map and bounds are now
  std::atomic, and bounds are published only after the bits are set so
  a concurrent drain always re-covers marked pages on its next pass.
@midwan

midwan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef58cf9a97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/osdep/picasso96.cpp Outdated
picasso_getwritewatch() returns page-aligned addresses, but the region
base was returned as the unaligned base + screen offset. With a panned
(SetPanning) screen offset that is not page-aligned, the page holding
the top-left of the visible screen compares below the returned base, so
the range check in picasso_flushpixels() rejects it after its dirty bit
was already cleared, leaving updates near the top-left stale.

Round the base down to the page boundary (offset & ~gwwpagemask),
matching how picasso_flushpixels() itself initializes src_start[0].
@midwan

midwan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 942eeed35f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/osdep/picasso96.cpp Outdated
Comment thread src/osdep/picasso96.cpp
Review follow-up: making the two bound variables atomic does not make
the drain's two-store reset atomic with respect to mark_dirty(). A
writer can publish a page between the drain's load and its reset, so
the reset then erases the freshly published bound: the page stays set
but outside any published range and is not rendered until a later
write happens to republish a range.

Pack the bounds into a single word, (min_page << 32) | (max_page + 1):
mark_dirty() widens the packed value with a compare_exchange loop after
setting the page bits, and picasso_getwritewatch() claims the whole
range with one compare_exchange to the empty state, retrying against
the wider range if a concurrent widening made the exchange fail. A
claimed range therefore always includes every page published before
the claim, and pages marked after the claim are published anew for the
next frame.
Review follow-up: the dirty page list drained once per frame spans both
split regions (and can contain pages outside the visible screen, e.g.
offscreen bitmaps), but the full-copy heuristic compared that total
against each region's own size. A busy first region could push the
second region over the 80% threshold and trigger a needless full copy.
Count only the pages that fall inside the current region, matching the
region-scoped Windows GetWriteWatch, and skip the region entirely when
none apply.
Review follow-up: the per-region page filter must only apply to the
drained dirty list. In the forced full-refresh path (full_refresh < 0
after a mode change, flash or RTG clear) the page count is synthetic
and gwwbuf is filled with generated region pages, so filtering there
could only ever discard required work. Move the filter into the
drain branch and count the synthetic list directly.
Review follow-up: shortening gwwcnt to the region-filtered count made
the copy loop examine only a prefix of the page-ordered list, but
foreign pages (lower split region, offscreen bitmaps) can occupy that
prefix, so matching pages later in the list were skipped after their
dirty bits had already been cleared by the drain.

Keep gwwcnt untruncated for iteration - the copy loop range-checks
each entry itself - and use the filtered count only for the skip
decision and the full-copy heuristic. Compacting the list in place
instead would destroy the entries the second split region still has
to examine.
Review follow-up: draining a page with a separate load and store could
lose a concurrent write - the writer sets the bit and republishes the
bounds between the drain's load and its unconditional clear, so the
next drain claims the republished bounds but finds no set bit and never
uploads the new pixels.

Use exchange(false) so the test and the clear are one read-modify-write:
a mark made before the exchange is consumed by this drain (the copy
reads VRAM after it), and a mark made after it stays set for the next
drain.
@midwan
midwan merged commit 7f57d50 into master Sep 16, 2026
11 of 12 checks passed
@midwan
midwan deleted the fix/27-rtg-mode-switch-dirty-pages branch September 16, 2026 04:44
midwan added a commit to BlitterStudio/amiberry that referenced this pull request Sep 16, 2026
…2335)

* fix(rtg): close race and split-screen holes in dirty-page tracking

Found by review of the equivalent fix in amiberry-lite (PR
BlitterStudio/amiberry-lite#44); the same issues exist here:

- picasso_getwritewatch(): with VGA screen splitting active the getter
  was called once per source region but drained the whole dirty map on
  the first call, so pages belonging to the wrapped lower region were
  dropped by the first pass's range filter and missing from the second.
  Drain once per frame and reuse the page list for both regions.
- picasso_getwritewatch(): return the region base as board base plus
  the screen offset, matching the Windows GetWriteWatch semantics.
  Returning the bare board base let the caller's range filter accept
  pages below the visible screen (offscreen bitmaps, split region).
- mark_dirty()/picasso_getwritewatch(): synchronize the dirty bounds
  with the RTG render thread. With gfxcard_multithread enabled,
  mark_dirty() runs from the CPU write handlers while render_thread()
  drains the map, racing on the bounds (and allowing a drain to reset
  the bounds between a writer's compare and its bit-set, losing the
  page until another write covers it). The map and bounds are now
  std::atomic, and bounds are published only after the bits are set so
  a concurrent drain always re-covers marked pages on its next pass.

Also fix the mman_GetWriteWatch declaration to match the real API
(PULONG_PTR lpdwGranularity).

* fix(rtg): page-align the dirty-page region base

picasso_getwritewatch() returns page-aligned addresses, but the region
base was returned as the unaligned base + screen offset. With a panned
(SetPanning) screen offset that is not page-aligned, the page holding
the top-left of the visible screen compares below the returned base, so
the range check in picasso_flushpixels() rejects it after its dirty bit
was already cleared, leaving updates near the top-left stale.

Round the base down to the page boundary (offset & ~gwwpagemask),
matching how picasso_flushpixels() itself initializes src_start[0].

* fix(rtg): claim the dirty range with a single compare_exchange

Review follow-up: making the two bound variables atomic does not make
the drain's two-store reset atomic with respect to mark_dirty(). A
writer can publish a page between the drain's load and its reset, so
the reset then erases the freshly published bound: the page stays set
but outside any published range and is not rendered until a later
write happens to republish a range.

Pack the bounds into a single word, (min_page << 32) | (max_page + 1):
mark_dirty() widens the packed value with a compare_exchange loop after
setting the page bits, and picasso_getwritewatch() claims the whole
range with one compare_exchange to the empty state, retrying against
the wider range if a concurrent widening made the exchange fail. A
claimed range therefore always includes every page published before
the claim, and pages marked after the claim are published anew for the
next frame.

* fix(rtg): filter the reused dirty-page count per split region

Review follow-up: the dirty page list drained once per frame spans both
split regions (and can contain pages outside the visible screen, e.g.
offscreen bitmaps), but the full-copy heuristic compared that total
against each region's own size. A busy first region could push the
second region over the 80% threshold and trigger a needless full copy.
Count only the pages that fall inside the current region, matching the
region-scoped Windows GetWriteWatch, and skip the region entirely when
none apply.

* fix(rtg): keep forced full refreshes out of the region filter

Review follow-up: the per-region page filter must only apply to the
drained dirty list. In the forced full-refresh path (full_refresh < 0
after a mode change, flash or RTG clear) the page count is synthetic,
so filtering there could only ever discard required work. Move the
filter into the drain branch and count the synthetic list directly.

Also restore the missing gwwbuf fill in the full-refresh branch
(WinUAE parity, lost in a refactor): the copy loop consumes gwwbuf
entries, and without the fill it iterated stale pointers from the last
drain, which the region range checks would mostly filter out after a
mode change - defeating the forced full copy.

* fix(rtg): iterate the full dirty list when copying regions

Review follow-up: shortening gwwcnt to the region-filtered count made
the copy loop examine only a prefix of the page-ordered list, but
foreign pages (lower split region, offscreen bitmaps) can occupy that
prefix, so matching pages later in the list were skipped after their
dirty bits had already been cleared by the drain.

Keep gwwcnt untruncated for iteration - the copy loop range-checks
each entry itself - and use the filtered count only for the skip
decision and the full-copy heuristic. Compacting the list in place
instead would destroy the entries the second split region still has
to examine.

* fix(rtg): clear dirty bits with an atomic exchange

Review follow-up: draining a page with a separate load and store could
lose a concurrent write - the writer sets the bit and republishes the
bounds between the drain's load and its unconditional clear, so the
next drain claims the republished bounds but finds no set bit and never
uploads the new pixels.

Use exchange(false) so the test and the clear are one read-modify-write:
a mark made before the exchange is consumed by this drain (the copy
reads VRAM after it), and a mark made after it stays set for the next
drain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Switching between RTG modes and native modes doesn't update screen

1 participant