T-505: the long page's wrong verify frame found and fixed (engine 0073); the second review's fixes

The one frame of 58 with 13 wrong rows (PICKUP 2026-10-26, item 2) reproduced on the device with the
verify dumps: the base frame's record of what lay beneath its header held the header itself. Its job
was the load's tree change (a 4 px strip), composed by the engine as a partial composite of those
columns but read whole rows: the other columns were the window target's last composite, the verify
reference with the parts. Engine 0073 composes a content-only frame's changed rows whole. Found the
same day by the inspector's review (Codex out of credits until Nov 4th), whose other findings are
fixed too: every part's beneath recorded before any is drawn and put back in reverse (F3), the main
lane's second job only on a base of its own kind (F4). cpuPartsDress and the main poll log the
restore (the base record, d, the rows put back, a pixel of the record), rate-limited.

Records: docs/rounds/t505-2026-11-02/notes.txt, context/impl/impl-review-findings-t505-cpuparts.md
(review 2). Verified: the long page's scripted pan under JIHAD_GPU_VERIFY=1 on the Enyo variant, 85 frames, 78
shifted, 0 wrong rows (2 wrong in each round before the fix); frames looked at. Kit: R4's CPU-parts
criterion met on the long page too, R2 gains the read-width criterion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
NotAlexNoyleandClaude Fable 5.1 committed 2026-11-02 20:30:35 -05:00
1 parent 1a0026e923
commit c29f396c21
16 files changed
+3967 -11

No files matched your search

@@ -0,0 +1,200 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: NotAlexNoyle <alexnoyle@icloud.com>
Date: Mon, 2 Nov 2026 20:27:22 -0500
Subject: [PATCH] gpu-compositor: a frame without its parts composes the
changed rows whole, as it reads them; the review's fixes
T-505, the long page's one wrong verify frame (2026-11-02) and the inspector's review of E2:
- F1: LayerManagerComposite::UpdateAndRender makes a composite whose tree change is smaller than the
frame a partial one, clipped and cleared to the change's bounds (the load's 4 px strip at x 764..768,
an image arriving), while a frame without its parts reads the changed rows whole (review D4 of 0072
turned its column read off). The columns beside the change came from the window target's last
composite: in a verify run the whole reference with the parts, so the header passed for the page
beneath it, the daemon recorded it as what lay beneath, and the first shift put it back where the
old header landed. JihadWholeRows, set by the job's content-only scope: the diff-driven partial
composite of such a job covers the changed rows across the frame's width, and reports them so.
- F2: one sample time for the job (mTestTime, the test API's mechanism, restored after): an animated
part is measured, pictured and composed at the same instant.
- F3: JihadMergeFixed joins rects again after the reduction's unions, so no two overlap.
- F5: the parts' signature mixes where a part cut by the frame's edge starts, every part's clip rect
and the mask layer's identity and bounds; a mix-blend-mode part marks the frame covered.
- F6: the parts pass runs under the job's cancel flag.
Verified on the device: the long page's scripted pan with JIHAD_GPU_VERIFY=1, where the frame built on
the load's strip composite used to carry the header beneath (pan-v2, pan-v3: 2 wrong frames each),
85 frames verified, 78 shifted, 0 wrong rows (docs/rounds/t505-2026-11-02/notes.txt).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---
.../composite/LayerManagerComposite.cpp | 10 ++++
gfx/layers/composite/LayerManagerComposite.h | 4 ++
gfx/layers/ipc/CompositorBridgeParent.cpp | 52 ++++++++++++++++---
gfx/layers/ipc/CompositorBridgeParent.h | 2 +-
4 files changed, 59 insertions(+), 9 deletions(-)
diff --git a/gfx/layers/composite/LayerManagerComposite.cpp b/gfx/layers/composite/LayerManagerComposite.cpp
index 280b884634..1dfc596edc 100644
--- a/gfx/layers/composite/LayerManagerComposite.cpp
+++ b/gfx/layers/composite/LayerManagerComposite.cpp
@@ -564,6 +564,16 @@ LayerManagerComposite::UpdateAndRender()
mJihadPartialOn = true;
} else if (!mTarget && !mJihadLastChanged.IsEmpty() && mClonedLayerTreeProperties &&
!invalid.Contains(mRenderBounds)) {
+ if (mJihadWholeRows) {
+ // T-505: a frame without its parts reads the changed rows whole (no column read, review D4): the
+ // composite covers them whole too, or the columns beside the change keep what the target held
+ // from its last composite (a frame with its parts: their pixels then pass for the page beneath
+ // them, and a later shift shows them; found 2026-11-02 on the long page's first shifted frame)
+ const gfx::IntRect b = invalid.GetBounds();
+ const gfx::IntRect rows = gfx::IntRect(mRenderBounds.x, b.y, mRenderBounds.width, b.height).Intersect(mRenderBounds);
+ invalid = nsIntRegion(rows);
+ mJihadLastChanged = rows;
+ }
mJihadPartialOn = true; // only what changed, when that is less than the frame
}
if (invalid.IsEmpty() && !mWindowOverlayChanged) {
diff --git a/gfx/layers/composite/LayerManagerComposite.h b/gfx/layers/composite/LayerManagerComposite.h
index c49ecb73a6..0d6e7ebf39 100644
--- a/gfx/layers/composite/LayerManagerComposite.h
+++ b/gfx/layers/composite/LayerManagerComposite.h
@@ -141,6 +141,9 @@ public:
// the layer diff reports as the content's whole bounds): the diff is dropped, the bands asked
// for are composed and read
void JihadIgnoreDiff(bool aOn) { mJihadIgnoreDiff = aOn; }
+ // T-505: a frame without its parts is read back in whole rows (no column read): a partial composite
+ // of what the tree changed covers those rows across the frame's width, not the change's columns
+ void JihadWholeRows(bool aOn) { mJihadWholeRows = aOn; }
// round 115: the next diff treats the root-content layer's scroll as a shift (LayerTreeInvalidation.h)
void JihadCompensateScroll(bool aOn) { mJihadCompensateScroll = aOn; }
// round 119: the tree diff now, without a render (the stored tree refreshed as a composite
@@ -398,6 +401,7 @@ private:
gfx::IntRect mJihadRegionRect[12];
int mJihadRegionN = 0;
bool mJihadIgnoreDiff = false;
+ bool mJihadWholeRows = false; // T-505: the rows read back are whole rows
bool mJihadCompensateScroll = false;
bool mJihadPartialOn = false; // this Render is a partial composite
UniquePtr<LayerProperties> mClonedLayerTreeProperties;
diff --git a/gfx/layers/ipc/CompositorBridgeParent.cpp b/gfx/layers/ipc/CompositorBridgeParent.cpp
index 420a0c7cbf..9aa08d7f9d 100644
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -1174,7 +1174,7 @@ CompositorBridgeParent::JihadHideForJob()
// leaf drawn after a part (above it, in paint order) whose pixels meet it is a cover: a popup over a fixed bar, a page
// over a fixed background
uint32_t
-CompositorBridgeParent::JihadPartsSig(const gfx::IntRect* aRects, int aN, bool* aCovered)
+CompositorBridgeParent::JihadPartsSig(const gfx::IntRect* aRects, int aN, bool* aCovered, int aWidth, int aHeight)
{
uint32_t h = 2166136261u;
auto mix = [&](uint32_t v) { h = (h ^ v) * 16777619u; };
@@ -1207,12 +1207,21 @@ CompositorBridgeParent::JihadPartsSig(const gfx::IntRect* aRects, int aN, bool*
const bool part = l->GetIsFixedPosition() || l->GetIsStickyPosition();
if (part || depth > 0) {
mix((uint32_t)(uintptr_t)l); mix((uint32_t)l->GetType()); mixf(l->GetOpacity());
- mix(l->GetContentFlags()); mix(l->GetMaskLayer() ? 1u : 0u);
+ mix(l->GetContentFlags());
+ if (Layer* mk = l->GetMaskLayer()) { mix((uint32_t)(uintptr_t)mk); mixr(mk->GetVisibleRegion().GetBounds().ToUnknownRect()); } // (review F5)
mixr(l->GetVisibleRegion().GetBounds().ToUnknownRect());
+ if (l->GetClipRect()) mixr(l->GetClipRect()->ToUnknownRect()); // (review F5) a top-level part's clip too
if (depth > 0) {
const gfx::Matrix4x4 m = l->GetBaseTransform();
for (int k = 0; k < 16; k++) mixf(m.components[k]);
- if (l->GetClipRect()) mixr(l->GetClipRect()->ToUnknownRect());
+ }
+ if (l->GetMixBlendMode() != gfx::CompositionOp::OP_OVER) *aCovered = true; // (review F5) not drawable by "over" on the CPU
+ if (part && depth == 0) {
+ // (review F5) a part cut by the frame's top or bottom edge: its rect keeps its size while the slice shown
+ // moves with it (a sticky element not yet stuck, taller than the frame or half off it): where it starts
+ const gfx::IntRect vb = l->GetLocalVisibleRegion().GetBounds().ToUnknownRect();
+ const gfx::IntRect tb = gfx::RoundedOut(l->GetEffectiveTransform().TransformBounds(gfx::Rect(vb.x, vb.y, vb.width, vb.height)));
+ if (tb.y < 0 || tb.YMost() > aHeight) mix((uint32_t)tb.y);
}
if (l->GetType() == Layer::TYPE_COLOR) {
const gfx::Color c = static_cast<ColorLayer*>(l)->GetColor();
@@ -1556,6 +1565,19 @@ JihadMergeFixed(gfx::IntRect* aR, gfx::IntRect* aK, int aN, int aMax)
}
aR[bi] = aR[bi].Union(aR[bj]); aK[bi] = gfx::IntRect();
remove(bj);
+ // (review F3) the union may now overlap a third rect: joined again, so no two rects overlap (the daemon's
+ // beneath records and restores take each rect's pixels once)
+ for (bool merged = true; merged && aN > 1;) {
+ merged = false;
+ for (int p = 0; p < aN && !merged; p++) {
+ for (int q = p + 1; q < aN && !merged; q++) {
+ if (!aK[p].IsEmpty() || !aK[q].IsEmpty()) continue;
+ gfx::IntRect a = aR[p];
+ a.Inflate(1);
+ if (a.Intersects(aR[q])) { aR[p] = aR[p].Union(aR[q]); remove(q); merged = true; }
+ }
+ }
+ }
}
return aN;
}
@@ -1906,11 +1928,22 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal
if (b->mLayerManager) b->mLayerManager->JihadHoldBaseline(false);
}
} jihadVerifyGuard(this, aReq && aReq->verify);
- // T-505: a frame without its parts (flag 128), for this job's whole run
+ // T-505: a frame without its parts (flag 128), for this job's whole run; its reads are whole rows, so
+ // a partial composite of a change is whole rows too (LayerManagerComposite::JihadWholeRows)
struct JihadContentOnlyScope {
- explicit JihadContentOnlyScope(bool aOn) { sJihadContentOnly = aOn; }
- ~JihadContentOnlyScope() { sJihadContentOnly = false; }
- } jihadContentOnlyScope(aReq && aReq->contentOnly);
+ LayerManagerComposite* m;
+ JihadContentOnlyScope(LayerManagerComposite* aM, bool aOn) : m(aM) { sJihadContentOnly = aOn; if (m) m->JihadWholeRows(aOn); }
+ ~JihadContentOnlyScope() { sJihadContentOnly = false; if (m) m->JihadWholeRows(false); }
+ } jihadContentOnlyScope(mLayerManager, aReq && aReq->contentOnly);
+ // (review F2) one sample time for the job's prep, its parts pass and its frame: an animated part is measured,
+ // pictured and composed at the same instant (mTestTime is the mechanism the test API uses; restored after)
+ struct JihadJobTimeScope {
+ CompositorBridgeParent* b; bool was; TimeStamp wasTime;
+ JihadJobTimeScope(CompositorBridgeParent* aB, bool aOn) : b(aB), was(aB->mIsTesting), wasTime(aB->mTestTime) {
+ if (aOn && !was) { b->mIsTesting = true; b->mTestTime = TimeStamp::Now(); }
+ }
+ ~JihadJobTimeScope() { if (!was) { b->mIsTesting = false; b->mTestTime = wasTime; } }
+ } jihadJobTimeScope(this, aReq && aReq->contentOnly);
if (aReq) aReq->partsGen = gJihadPartsGen;
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
if (!aReq || !mCompositor || !mLayerManager) {
@@ -1999,7 +2032,7 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal
mLayerManager->JihadPrepareOnly();
const int partN = JihadFixedRectsNow(aWidth, aHeight, parts, pk); // every fixed and sticky layer, whole rects
bool covered = false;
- aReq->partsGen = JihadPartsSig(parts, partN, &covered);
+ aReq->partsGen = JihadPartsSig(parts, partN, &covered, aWidth, aHeight);
if (covered) aReq->partsFlags |= 1;
{
sJihadContentOnly = false; // the card's hold set keeps T-544's meaning: fixed layers and stuck ones
@@ -2044,6 +2077,8 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal
aReq->partsFlags |= 2;
} else {
JihadVerifyGuard partsGuard(this, true); // the baseline, the probe, the last scroll and view held
+ struct PartsCancelScope { CompositorOGL* o; ~PartsCancelScope() { o->JihadSetCancel(nullptr); } } partsCancelScope{ogl};
+ ogl->JihadSetCancel(&aReq->cancelled); // (review F6) a job given up writes no more of its buffer
const int passRows = ogl->JihadBeginBandPass(aWidth, used);
if (!passRows) {
aReq->partsFlags |= 2;
@@ -2074,6 +2109,7 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal
for (int k = 1; k < aReq->nStuck; k++)
off[k] = off[k - 1] + (size_t)(aReq->stuck[k - 1][2] - aReq->stuck[k - 1][0]) * (aReq->stuck[k - 1][3] - aReq->stuck[k - 1][1]) * 4;
for (int i = 0; i < np && ok; i++) {
+ if (aReq->cancelled) { ok = false; break; } // (review F6)
const int k = pieces[i].k, rw = aReq->stuck[k][2] - aReq->stuck[k][0];
uint8_t* d = aReq->partsDest + off[k] + (size_t)(pieces[i].y - aReq->stuck[k][1]) * rw * 4;
ok = ogl->JihadReadPacked(d, rw * 4, passRows, pieces[i].atX, pieces[i].w, pieces[i].atY, pieces[i].h);
diff --git a/gfx/layers/ipc/CompositorBridgeParent.h b/gfx/layers/ipc/CompositorBridgeParent.h
index 5776ec30c4..ce9fa9ccf2 100644
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -548,7 +548,7 @@ public:
void JihadHideForJob(); // Jihad T-504/T-505: the overview's and a content-only frame's layers hidden
// Jihad T-505 (E2): the parts' signature (their layers, content, attributes and sizes); *aCovered when a content
// layer is drawn above a part (the daemon cannot draw that part over the frame)
- uint32_t JihadPartsSig(const gfx::IntRect* aRects, int aN, bool* aCovered);
+ uint32_t JihadPartsSig(const gfx::IntRect* aRects, int aN, bool* aCovered, int aWidth, int aHeight);
int JihadFixedRectsNow(int aWidth, int aHeight, gfx::IntRect* aOut, gfx::IntRect* aKeep);
void JihadUpdateLastFixed(int aWidth, int aHeight);
bool mJihadSeqKnown = false;
+1 -1
View File
@@ -9,7 +9,7 @@ UXP_BASE=5f290d1097a8350b56ce2092aa9d5fc9147c6661
# The tip of our engine branch (NotAlexNoyle/UXP-Jihad, jihad/uxp-<UXP_TAG>: one commit per patch
# in build/desktop/patches) the build uses. The third_party/uxp gitlink and the OE recipe's SRCREV
# must both be this commit.
UXP_PIN=446b22790a48c38c0ae4d523d1603e63beb88497
UXP_PIN=1f54df3762f255dfe8622d98150835ce3f64f228
# What the user agent and the start pages say: Goanna version and the short UPSTREAM base.
GOANNA_VERSION=6.9
UXP_SHORT=5f290d1
@@ -34,7 +34,7 @@ COMPATIBLE_MACHINE = "(tenderloin|opal)"
# for real (build/engine/patch-queue.sh --check proves it reproduces SRCREV's tree):
# SRC_URI = "git://repo.palemoon.org/MoonchildProductions/UXP.git;protocol=https;tag=<UXP_TAG>"
SRC_URI = "git://${JIHAD_REPO}/third_party/uxp;protocol=file;nobranch=1"
SRCREV = "446b22790a48c38c0ae4d523d1603e63beb88497"
SRCREV = "1f54df3762f255dfe8622d98150835ce3f64f228"
S = "${WORKDIR}/git"
# Modern C++14 toolchain (device-build R1) — the stock dylan/device gcc cannot build UXP.
@@ -49,3 +49,27 @@ the verify guard's state, a page-made scroll is whole). No path publishes a fram
Proposed criterion (cavekit-gpu-rendering.md): every published frame's CPU-drawn parts come from a composite of the
same tree state as the frame; the change signal covers content, attribute, structure and animation changes to a
part; `JIHAD_GPU_VERIFY=1` with the parts on shows zero wrong rows over a scripted pan on Wikipedia and the long page.
## Review 2 — the inspector on main `81f56bef` + engine `446b22790a` (2026-11-02; Codex out of credits until Nov 4th)
The review gate PICKUP 2026-10-26 (night) owed (prompt: `.cavekit/handoff-2026-10-26/codex-e2.prompt`; `codex exec` on
gpt-5.6-sol and gpt-5.6-terra both answered "usage limit ... try again at Nov 4th, 2026 1:42 AM", so ONE ck:inspector took
it). It also named the cause of the long page's one wrong verify frame, found independently on the device the same day
(`docs/rounds/t505-2026-11-02/notes.txt`). Verdict: REVISE (P1 1, P2 3, P3 2).
| # | sev | where | finding | fix | status |
|---|---|---|---|---|---|
| F1 | high | engine LayerManagerComposite::UpdateAndRender; CompositorBridgeParent 2666 | a frame without its parts whose tree change is narrower than the frame (the load's 4 px strip at x 764..768, an image arriving) takes the whole path as a PARTIAL composite clipped and cleared to the change's columns, but is read back in whole rows (review D4 turned the column read off without the parts): the columns beside the change come from the window target's last composite — in a verify run the whole reference WITH the parts, so the header passes for the page beneath it, the dress records it as beneath, and the first shift puts it back where the old header landed (the 13 wrong rows at 0..13 of 2026-10-26; 8 at 133..141 and 14 at 0..14 on 2026-11-02, dumps looked at: header blue where the reference has the page's background); outside verify runs the stale columns are another frame's rows | `LayerManagerComposite::JihadWholeRows`: for a job without its parts the diff-driven partial composite covers the changed rows across the frame's width and reports them so (`mJihadLastChanged`); set for the job by the content-only scope | fixed (engine 0073) — device: the same round, every record's probe the page's background, 0 wrong rows (below) |
| F2 | medium | engine JihadComposeAndRead (the prep's TransformShadowTree at the last composite's time; each ForceComposeToTarget at now) | an animated part (a header sliding away) is measured (rects, signature, hold set) at an older time than its picture and its frame are composed: the picture cut at the old rects | one sample time for the job: `mIsTesting`/`mTestTime` (the test API's mechanism, the only readers of the time) set to now for a content-only job and restored after | fixed (engine 0073); no animated-part page in the test set: by reading |
| F3 | medium | engine JihadMergeFixed; daemon cpuPartsDress | after the reduction past four rects, a union can overlap a third rect (five or more fixed/sticky layers); the daemon recorded rect i+1's beneath after drawing rect i and restored forward: the overlap blended twice, a ghost | engine: the rects joined again after each union; daemon: every part's beneath recorded before any is drawn, put back in reverse order | fixed (engine 0073, daemon); by reading (no five-part page in the test set) |
| F4 | medium | daemon gpuJobStart (round 130's second job) | the second job checks only its own kind: when the parts turned off between the two posts (the express thread's cover refusal, the document's third miss) a job with the engine's parts copies a frame without them while this thread dresses it (C1 on the main lane) | `mGpuNextBaseContentOnly` recorded with the base; the second job only on a base of its kind | fixed (daemon); by reading |
| F5 | low | engine JihadPartsSig | missed: a part cut by the frame's edge whose slice moves (its rect keeps its size); a top-level part's clip rect; the mask layer beyond its existence; a mix-blend-mode part cannot be drawn with "over" on the CPU | the slice's start mixed when a part crosses the frame's top or bottom; every part's clip rect; the mask's identity and bounds; a blend mode other than over marks the frame covered (the engine's parts) | fixed (engine 0073); by reading |
| F6 | low | engine the parts pass (JihadReadPacked) | the pass ran without the job's cancel flag: a job given up kept writing its buffer, which the daemon lends again after 10 s or deletes | the cancel flag set around the pass, checked before each piece's copy | fixed (engine 0073); by reading |
Checked and fine (the inspector): the packing; JihadReadPacked's bounds and flip; the dress's rect and size checks; the
beneath indexing; the state the parts pass leaves behind (baseline, probe, world, bounds, band, target, flags); the
buffers' bookkeeping under the frame lock; the express lane's C1 gate.
Proposed criterion (cavekit-gpu-rendering.md R2): every row a frame reads back is composed in that frame's own
composite across the width it reads; the verify round includes a narrow, tall change on an own-slot frame (the load's
strip is one).
+3 -1
View File
@@ -52,6 +52,7 @@ Hacker News 36-37, desktop Wikipedia 13-14 (software 9-12.4).
- [x] The mid-pan frame dumps of each are looked at and match the page (memory `verify-device-frames-by-eye`) *(2026-10-24, T-519: frames 60, 120 and 180 of each pan, `docs/rounds/t519-2026-10-24/pan-*-gpu-frame-*.jpg`, looked at: the headers and footers in place, the text continuous and numbered in order, the sticky sidebar and its opaque box in place over the stripes, the animated box and the overlay drawn, the self-scrolling page's counter, Wikipedia's text and its right-hand panel, Hacker News' last rows (the footer and search box) in frame 60 and white past the page's end in the later frames (the pan overshoots a 1249 px page, as recorded).)*
- [x] A page laid out wider than the card (fit zoom below 1) shows its right side and bottom *(rounds 129-134, T-502: the root's composition is its scroll port on the GPU path and the displayport covers what the card shows. 2026-10-24, T-519: news.ycombinator.com at the fit zoom 0.9552 (`displayport: scroll port 764x942 CSS px, shown 804x986`): frame 60 of the pan has the footer bar across the frame's whole width and the page's last rows; the only mark beyond the page is a 1 px light column at the frame's right edge, present in the T-505 dumps (r200) too.)*
- [x] A fast flick (a shift of more than 256 rows a frame) and two cards panned in turn pass the verify mode (added 2026-10-19 by T-513's review: the band scratch shared between compositors, the band frame drawn with the wrong surface current) *(2026-10-19: six 700 ms flicks of 6000 px on `jihad-long-static.html`, 40 verified frames with shifts to 1200 rows, 0 rows wrong; two cards in one daemon panned in turn (the long page, then `jihad-sticky-mix.html` in a second card, then the first again: `tools/device/t502/two-cards.sh`, the card brought back by the Enyo test hook's `A` step and the inject channel's `target focus`), 26 frames, shifts to 1130, 0 rows wrong. The verify reference is now composed right behind its job, on the same layer tree: composed at the job's poll it met a tree the flick had moved on from (the displayport's tiles gone) and called right frames wrong, 744 rows in the first run. Again on 2026-10-24 on the build with Codex's fixes: two cards 22 frames, six flicks 40 frames with 15 shifts over 256 rows, 0 rows wrong. Notes `docs/rounds/t513-2026-10-19/`.)*
- [x] Every row a frame reads back is composed in that frame's own composite across the width it reads: a partial composite covers the rows read, never only the change's columns while whole rows are read; the verify round includes a narrow, tall change on an own-slot frame (the load's strip beside the scroll port is one) *(added 2026-11-02 from the inspector's review 2 of T-505's E2 and the long page's wrong verify frame: engine 0073 (`LayerManagerComposite::JihadWholeRows`); the long page's scripted pan under `JIHAD_GPU_VERIFY=1` with the parts on the CPU, whose load strip takes that path: 85 verified frames, 78 shifted, 0 wrong rows, the base records' probes the page's pixels (`docs/rounds/t505-2026-11-02/notes.txt`); 2 wrong frames in each of the two rounds before the fix)*
**Dependencies:** R1
### R3: Pans meet the chart, on real pages too
@@ -71,7 +72,7 @@ Hacker News 36-37, desktop Wikipedia 13-14 (software 9-12.4).
- [x] A page with a fixed header and a fixed footer reads two bands, not the whole frame *(2026-10-24, T-519: `jihad-fixed-both.html` (an opaque fixed header and footer, nothing else fixed): every band frame reads two bands, `rows 139..141 and 1033..1224`, `rows 130..171 and 1024..1224`, with the header and footer kept.)*
- [x] Fixed and stuck content stays put on the card between frames too, while the card moves the frame it holds for a pan *(2026-10-26, T-544, engine 0068: the daemon tells the card each frame's fixed and stuck rects and the card draws them where the frame did; desktop Wikipedia's sidebars by finger at the same place in four mid-drag screenshots of four while the column moved 47-150 px (displaced 11-12 px in two of four without it), no ghost by eye)*
- [x] R2's verify passes on each case *(2026-10-24, T-519: 0 rows structurally wrong on `jihad-long-static.html`, `jihad-fixed-both.html`, `jihad-sticky-mix.html` and desktop Wikipedia under `JIHAD_GPU_VERIFY=1` over the 10 s pan; the per-page lines are in R2.)*
- [~] When the parts are drawn on the CPU (T-505, the owner's CPU/GPU split): every published frame's CPU-drawn parts come from a composite of the same tree state as the frame; the change signal covers content, attribute, structure and animation changes to a part; content drawn above a part is never drawn under it; `JIHAD_GPU_VERIFY=1` with the parts on shows zero wrong rows over a scripted pan on desktop Wikipedia and the long page *(added 2026-10-26 from the inspector's review, `context/impl/impl-review-findings-t505-cpuparts.md`. Engine 0072 (E2): the parts' picture read in the frame's own job when their signature moves; desktop Wikipedia verified, 27 shifted frames, 0 wrong rows during the pan (`docs/rounds/t505-2026-10-26/pan-v1-wiki-daemon-gpu.log`); the long page's verify run still owed)*
- [x] When the parts are drawn on the CPU (T-505, the owner's CPU/GPU split): every published frame's CPU-drawn parts come from a composite of the same tree state as the frame; the change signal covers content, attribute, structure and animation changes to a part; content drawn above a part is never drawn under it; `JIHAD_GPU_VERIFY=1` with the parts on shows zero wrong rows over a scripted pan on desktop Wikipedia and the long page *(added 2026-10-26 from the inspector's review, `context/impl/impl-review-findings-t505-cpuparts.md`. Engine 0072 (E2): the parts' picture read in the frame's own job when their signature moves; desktop Wikipedia verified, 27 shifted frames, 0 wrong rows during the pan (`docs/rounds/t505-2026-10-26/pan-v1-wiki-daemon-gpu.log`); 2026-11-02: the long page's verify run done — its one wrong frame (13 rows at the first shift, 2026-10-26) reproduced with the verify dumps and traced to the engine: a content-only frame's narrow tree change (the load's 4 px strip) was composed as a partial composite of those columns but read whole rows, the other columns the window target's last composite (the verify reference with the parts), so the header passed for the page beneath it (engine 0073 composes the rows whole); after it 85 verified frames, 78 shifted, 0 wrong rows (`docs/rounds/t505-2026-11-02/notes.txt`, pan-v4-long); the inspector's review of E2 (`context/impl/impl-review-findings-t505-cpuparts.md`, review 2) fixed too)*
**Dependencies:** R2
### R5: Animation and new content meet the chart
@@ -158,3 +159,4 @@ Hacker News 36-37, desktop Wikipedia 13-14 (software 9-12.4).
- 2026-10-25: R8's 24 fps criterion met (the partial redraw through the frame trailer, the tall band surface, engine 0063); R3's gap criterion annotated with the trace.
- 2026-10-26: R6's first-frame criterion met (T-504, the owner's bounded overview: a double tap out after a pan while zoomed in drawn at once from a half-resolution picture; T-516's double taps in within 250 ms).
- 2026-10-26: R6 gains the overview criterion (no uncovered pixel, nothing stale beneath see-through parts) from T-504's review.
- 2026-11-02: R4's CPU-parts criterion met on the long page too (its wrong verify frame traced to a partial composite narrower than the rows read, engine 0073); R2 gains the read-width criterion from the inspector's review 2 of T-505's E2.
+1 -1
View File
@@ -594,7 +594,7 @@ the mid-pan dumps, never by a screenshot. No device run while the owner is at th
| T-502 | The spike and the compositor-side scroll (rounds 1-137): patches 0040/0041/0042, the daemon's job pipeline, band reads on the band surface, the displayport, fixed/sticky rows, the composition fix for wide pages | cavekit-gpu-rendering.md | R2, R3 (part) | — | yes | L | DONE (commits through `bc4a8eaf`/`b30b9989`: test pan 27-34 fps, animation 26-31, HN 36-37, desktop Wikipedia 13-14 vs software 9-12.4) |
| T-503 | Fixed and sticky at minimum cost: opaque stuck layers kept (per opaque rect, the visible region's gaps), others read as columns, fixed header + footer as two bands; the draw-culling fix kept; verify and dumps on the long-static page, a header+footer page and desktop Wikipedia | cavekit-gpu-rendering.md | R4, R2 | T-502 | yes | M | DONE 2026-10-18 (rounds 138-150): opaque fixed/stuck layers kept per covered rect, the rest read as column chunks packed on the band surface, header + footer as two bands; the draw culling fixed; a sticky layer the engine laid out stuck counted (probed sticky ranges); the root composition covers what the card shows (Hacker News' blank bottom strip); verify made side-effect free with a structural test: 0 structurally wrong rows on the test page, `jihad-sticky-mix.html`, desktop Wikipedia and Hacker News. Pans: test page 27-34 fps, sticky-mix 15-17.5 (software 0.9-3.1), Wikipedia 16.4-18.5 (was 13-14; software 9-12.4), Hacker News 34-37.5, animation 24-32 |
| T-504 | Zoom keeps up: a scripted pinch and double-tap on Wikipedia and the test page with the frame timeline; first frame at the new zoom ≤ 250 ms; no grey at rest; tile uploads off the GPU's queue | cavekit-gpu-rendering.md | R6 | T-502 | yes | L | DONE 2026-10-26 (the owner's bounded overview: right after a zoom in the daemon composes a half-resolution picture of the page around the zoom-in point — the kept frame's rows and a frame's above and below, the scrolled content alone, rows no tile covered left out (engine 0069) — while the old tiles are still in the tree, and at a double tap out after a pan draws the card's view from it at twice its size with the fixed and stuck parts where they sit at the new position. The fixed and stuck parts are composed alone with their alpha and drawn over it where they sit at the new position. By finger on desktop Wikipedia (to 2.33, one or seven drags, back): the first frame 40-56 ms where the paint's came at 294-614 ms; frames looked at against the painted one, also on a page with a see-through fixed header and sticky sidebar; a pan past the picture still waits (the owner's bound, no black frame). An inspector review's six findings fixed (the readback's forced alpha, the parts' stale background, the probe, the scratch). Memory 1.8-2.8 MB and ~0.5 MB of parts beside the kept frame's 3.7; 7.5-9.4 MB of scratch for ~0.5 s. Cost: the zoom in's sharp frame later (682-779 ms against 519-545). `docs/rounds/t504-2026-10-26/notes.txt`) — earlier: IN PROGRESS 2026-10-26 (the owner: a bounded overview — a half-resolution picture of a few screens around the zoom-in point, ~4-8 MB per card) — earlier: PARTIAL 2026-10-18 (rounds 162-186: the zoom hold; the grey strip of a zoomed-in settle fixed; engine 0044 (tile textures converted at 1x1 and pooled: the Adreno's first write after a draw) and 0045 (no DEAA for axis-aligned scales: shader compiles in a zoom's first frame); the layers painted at the zoom's resolution above 1.1; double taps scripted (`t`), `JIHAD_GPU_ZOOM_DUMP`. Met: the scripted pinches and double taps with the timeline, the card covered at rest (dumps), uploads that do not wait, sharp zoomed text. Open: the first frame within 250 ms while the page's script holds the main thread (0.8-1.7 s on Wikipedia after its load) and one double tap in ten at ~400 ms: frames made off the main thread, with T-505); 2026-10-19, T-515: the zoom under script met (Wikipedia after load 10/10 within 225 ms); the double taps' slow ones are GPU draws: T-516; 2026-10-19 (rounds 334-341, `docs/rounds/t502-2026-10-17/dtap-r334-341.txt`): scripted double taps 2 of 18 over 250 ms (286, 367), the second core unplugged after an idle pause; the scripted tap skips the touch path's governor ramp, so a finger's tap is for T-514; a zoom's express frame now gives up uncomposed main-lane frames at the old zoom. Task token budget spent (the plugin's 45k): left PARTIAL for the owner) |
| T-505 | Real-page pans to the chart: desktop Wikipedia ≥ 24 fps; the 56 ms+ gaps traced (the main loop uncapped) and the avoidable ones removed; the per-page table against `NOGPU=1` | cavekit-gpu-rendering.md | R3 | T-503 | yes | L | IN PROGRESS 2026-10-26 (late: two inspector reviews (REVISE) -> E2, engine 0072: the parts' picture read in the frame's own job when their signature moves, exact (verify: 0 wrong rows in a Wikipedia pan, where the engine's parts showed 2 wrong frames); scripted Wikipedia 12.8 fps (9.4 without), the long page 23.6 (18.8); by finger Wikipedia 12.3 (9.5), the long page 18.3 (15.7) — the stale switch gone (as fast, exact); on by default once the re-review is clean (the owner's word); findings in `context/impl/impl-review-findings-t505-cpuparts.md`) — evening, the owner's "both, parts first": the parts on the CPU built behind `JIHAD_GPU_CPU_PARTS=1` (engine 0070, daemon 2bd3e6f1) and four faults on the way fixed (the picture never matching, the lanes' two kinds, engine 0071: own-slot frames diffed without the scroll, T-511's rule taking a slot stale whole); scripted pan with the card moving: desktop Wikipedia 9.4 -> 11.2 fps, 14.2 with `JIHAD_GPU_CPU_PARTS_STALE=1` (the owner's trade: the previous picture while a changed part's is made), the long page 18.8 -> 24.7; by finger Wikipedia 11-13 against ~10; frames looked at; whether to ship it on, and the stale trade, are the owner's; the CPU-composed bands next; `docs/rounds/t505-2026-10-26/notes.txt`) — earlier the same day: the cause proven and the card measured, the owner's "prove the cause first" and "measure first": the same daemon work makes desktop Wikipedia 32 fps and the long page 53 with LunaSysMgr drawing nothing, 8.6 / 13.6 with the card moving — the card's redraw on the shared GPU and memory makes every daemon stage ~3x slower (`JIHAD_GPU_NO_PUBLISH=1`, a diagnostic). The card's paint is Piranha's SOFTWARE blit (~10 ms, ~20 a second, most of them repaints); a surface kept per frame changed nothing; the frame wrapped without alpha halves the blit and cuts LunaSysMgr's CPU 15 % (shipped, `<state>/alphawrap` reverts) but not the pan rate. Codex's adversarial review: `context/impl/impl-review-findings-t505-codex.md`. The owner's plan, after "consider sharing tasks across the CPU and GPU": (1) pan frames composed on the GPU as the scrolled content alone and the fixed/sticky parts blended on the CPU, then (2) scroll-only bands composed on the CPU from the tiles, each behind a switch, measured by finger; `docs/rounds/t505-2026-10-26/notes.txt`) — earlier the same day, measured read-only, the owner's "measure first": under eight finger drags desktop Wikipedia 9.5 fps and the long page 16.6 while frames flow; a frame is bound by the readback path on the GPU shared with LunaSysMgr's redraw of the card — Wikipedia's band frames 64 ms median: the read's GPU wait 36, the copy from the base 16, the compose 14, its sticky sidebars read as columns doubling the read; the long page's 30 ms with 11 / 13 / 3; the long gaps are the job's GPU waits, none the main thread's; `docs/rounds/t505-2026-10-26/notes.txt`. The proposal is the owner's to choose) — earlier: IN PROGRESS 2026-10-26 (R3 re-opened against the corrected test, the owner's choice: the scripted pan never moved the card, so its 24 fps left out the card's own redraw; with the card moving desktop Wikipedia pans at ~7 fps under a finger and by the corrected scripted pan, the long page ~19 — the readback waits behind LunaSysMgr's redraw of the card on the same Adreno. Measured and not kept: the express thread's copy from the base (no gain), the card's scroll redraw coalesced to one in 33 or 50 ms (no gain, WebAppMgr's CPU unchanged). Earlier the same day: the owner: prototype the card shifting the frame it holds, the daemon sending only the new rows and the shift, behind a switch until measured; meanwhile desktop Wikipedia's pan is 24.0 fps with the sensors running since the second core comes up for a pan, T-516) — earlier: PARTIAL 2026-10-18 (rounds 187-201: the per-page table against `NOGPU=1` (the GPU path ahead on every page: the test page 30-35 against 2.7-3.2 fps, sticky-mix 17-19 against 2.4-3.1, desktop Wikipedia 19-26 against 2.7-2.9, Hacker News 31-37 against 6.4-10.4; the software path pans inside its painted region and publishes few frames); the 56 ms+ gaps traced to the page's restyle, the displayport moves' paints and misc events (stall sampler); overscan, 0.15 of the height above and below the view (engine 0046): grey-free pans, verify clean on four pages, at 10% of the rate (Wikipedia 16.5-24, the test page 25-32). Open: Wikipedia ≥ 24 fps, which needs the frames made off the main thread (T-515)); 2026-10-19, T-515: the main thread's stalls covered (19-21 fps, median gap 45 ms); the rest is the compositor thread's ~45 ms a Wikipedia frame: T-517; 2026-10-19 evening (rounds 349-357): the 10 s pan 23.2-24.1 fps; the page's tile uploads, partial upLine truncated
| T-505 | Real-page pans to the chart: desktop Wikipedia ≥ 24 fps; the 56 ms+ gaps traced (the main loop uncapped) and the avoidable ones removed; the per-page table against `NOGPU=1` | cavekit-gpu-rendering.md | R3 | T-503 | yes | L | IN PROGRESS 2026-11-02 (the review gate on 81f56bef + engine 446b22790a done by the inspector (Codex out of credits until Nov 4th): REVISE, six findings, all fixed (engine 0073; the daemon); PICKUP's item 2, the long page's one wrong verify frame, reproduced with the verify dumps and traced to the engine: a content-only frame's narrow tree change was composed as a partial composite of those columns but read whole rows, the other columns the window target's last composite (the verify reference with the parts), so the header passed for the page beneath it (F1; the fix composes the rows whole: `LayerManagerComposite::JihadWholeRows`); after it the long page's verify 85 frames, 78 shifted, 0 wrong (`docs/rounds/t505-2026-11-02/notes.txt`); CPU parts stay on by default (the owner's word, the review clean). Open: cheaper part pictures (a per-part signature, only the changed part read), then the CPU-composed bands) — earlier: IN PROGRESS 2026-10-26 (late: two inspector reviews (REVISE) -> E2, engine 0072: the parts' picture read in the frame's own job when their signature moves, exact (verify: 0 wrong rows in a Wikipedia pan, where the engine's parts showed 2 wrong frames); scripted Wikipedia 12.8 fps (9.4 without), the long page 23.6 (18.8); by finger Wikipedia 12.3 (9.5), the long page 18.3 (15.7) — the stale switch gone (as fast, exact); on by default once the re-review is clean (the owner's word); findings in `context/impl/impl-review-findings-t505-cpuparts.md`) — evening, the owner's "both, parts first": the parts on the CPU built behind `JIHAD_GPU_CPU_PARTS=1` (engine 0070, daemon 2bd3e6f1) and four faults on the way fixed (the picture never matching, the lanes' two kinds, engine 0071: own-slot frames diffed without the scroll, T-511's rule taking a slot stale whole); scripted pan with the card moving: desktop Wikipedia 9.4 -> 11.2 fps, 14.2 with `JIHAD_GPU_CPU_PARTS_STALE=1` (the owner's trade: the previous picture while a changed part's is made), the long page 18.8 -> 24.7; by finger Wikipedia 11-13 against ~10; frames looked at; whether to ship it on, and the stale trade, are the owner's; the CPU-composed bands next; `docs/rounds/t505-2026-10-26/notes.txt`) — earlier the same day: the cause proven and the card measured, the owner's "prove the cause first" and "measure first": the same daemon work makes desktop Wikipedia 32 fps and the long page 53 with LunaSysMgr drawing nothing, 8.6 / 13.6 with the card moving — the card's redraw on the shared GPU and memory makes every daemon stage ~3x slower (`JIHAD_GPU_NO_PUBLISH=1`, a diagnostic). The card's paint is Piranha's SOFTWARE blit (~10 ms, ~20 a second, most of them repaints); a surface kept per frame changed nothing; the frame wrapped without alpha halves the blit and cuts LunaSysMgr's CPU 15 % (shipped, `<state>/alphawrap` reverts) but not the pan rate. Codex's adversarial review: `context/impl/impl-review-findings-t505-codex.md`. The owner's plan, after "consider sharing tasks across the CPU and GPU": (1) pan frames composed on the GPU as the scrolled content alone and the fixed/sticky parts blended on the CPU, then (2) scroll-only bands composed on the CPU from the tiles, each behind a switch, measured by finger; `docs/rounds/t505-2026-10-26/notes.txt`) — earlier the same day, measured read-only, the owner's "measure first": under eight finger drags desktop Wikipedia 9.5 fps and the long page 16.6 while frames flow; a frame is bound by the readback path on the GPU shared with LunaSysMgr's redraw of the card — Wikipedia's band frames 64 ms median: the read's GPU wait 36, the copy from the base 16, the compose 14, its sticky sidebars read as columns doubling the read; the long page's 30 ms with 11 / 13 / 3; the long gaps are the job's GPU waits, none the main thread's; `docs/rounds/t505-2026-10-26/notes.txt`. The proposal is the owner's to choose) — earlier: IN PROGRESS 2026-10-26 (R3 re-opened against the corrected test, the owner's choice: the scripted pan never moved the card, so its 24 fps left out the card's own redraw; with the card moving desktop Wikipedia pans at ~7 fps under a finger and by the corrected scripted pan, the long page ~19 — the readback waits behind LunaSysMgr's redraw of the card on the same Adreno. Measured and not kept: the express thread's copy from the base (no gain), the card's scroll redraw coalesced to one in 33 or 50 ms (no gain, WebAppMgr's CPU unchanged). Earlier the same day: the owner: prototype the card shifting the frame it holds, the daemon sending only the new rows and the shift, behind a switch until measured; meanwhile desktop Wikipedia's pan is 24.0 fps with the sensors running since the second core comes up for a pan, T-516) — earlier: PARTIAL 2026-10-18 (roLine truncated
| T-506 | Animation, new content and self-scroll measured against the chart (DOM edit and typing ≤ 106 ms) | cavekit-gpu-rendering.md | R5 | T-502 | yes | S | DONE 2026-10-18 (rounds 202-203: the animation page 26-31 fps reading only its changed rows; typing 22-62 ms and DOM edits 26-70 ms from the input to the first published frame with changed rows (an input-latency probe on the inject channel); the self-scrolling page followed at 10.7-13.9 fps) |
| T-507 | Every other card event on the GPU path: rotation, resize, select popups, caret, link and find highlights, thumbnails, navigation without stale frames | cavekit-gpu-rendering.md | R9 | T-502 | yes | M | DONE 2026-10-18 (rounds 204-212 on `jihad-card-events.html`: rotation to landscape and back, the keyboard's resize, the select list and pick, the caret, find-in-page, link navigation with back, forward and reload, the thumbnail; the thumbnail's view row and the popups' origin corrected for overscan; the test hook gains `<`, `>`, `R`, `O<orientation>`; the inject channel `thumb <path>`) |
| T-508 | Video and Flash on the GPU path: `<video>` (VP8, Theora, H.264) to the end, styled video (rounded, fade, under a modal), Flash draw/animate/input | cavekit-gpu-rendering.md | R7 | T-502 | yes | L | DONE 2026-10-19 (rounds 213-228: VP8, Theora, VP9 and H.264 to the end on the GPU path, at the clips' rates; styled video (rounded, fade, modal); Flash draws, animates and takes keys; `push-engine-update.sh` now carries FFmpeg's libavcodec/libavutil, which a device updated by it lacked (H.264, AAC and MP3 were refused on both paths); new pages `jihad-video-styled.html`, `jihad-anim-opacity.html`, `jihad-canplay.html`) |
+1
View File
@@ -95,6 +95,7 @@ modifications; the same changes are the commits of `jihad/uxp-RB_20261006`, one
| `0070-gpu-compositor-a-frame-without-its-parts-flag-128-an.patch` | `widget/PuppetWidget.cpp`, `gfx/layers/opengl/CompositorOGL.h`, `gfx/layers/ipc/CompositorBridgeParent.{h,cpp}`, `gfx/layers/ipc/CompositableTransactionParent.cpp` | T-505: compose flag 128 (`jihad_offscreen_overview_ok` answers 2) — a frame without its parts: every fixed and sticky layer at opacity 0 in the composite and the diff, no fixed rows, keeps or columns, each part reported as its layer's whole rect; the parts' generation (compositable updates under a fixed or sticky layer) as the poll's thirtieth int; `JIHAD_GPU_REQLOG=1` names each composite request's source |
| `0071-gpu-compositor-the-diff-leaves-the-card-s-scroll-out.patch` | `gfx/layers/ipc/CompositorBridgeParent.cpp` | T-505: the diff leaves the card's scroll out of a frame with no base to copy (built on its slot's own frame) too, kept only when the shift it measured is the card's since the last composite; a layer transaction during a pan is no longer the whole frame changed for such frames |
| `0072-gpu-compositor-the-parts-picture-in-the-frame-s-own-.patch` | `widget/PuppetWidget.{h,cpp}`, `gfx/layers/opengl/CompositorOGL.{h,cpp}`, `gfx/layers/ipc/CompositorBridgeParent.{h,cpp}`, `gfx/layers/composite/LayerManagerComposite.{h,cpp}` | T-505 (E2): `jihad_offscreen_compose_post2`/`start2` (`overview_ok` answers 3): a frame without its parts reads the parts' picture in its own job when their signature (layers, content, attributes, sizes) is not the daemon's — the parts alone, packed on the band surface, read with their alpha; the poll's 49 ints (signature, the pass's bytes, flags: a content layer above a part / too big, the card's hold set); no column reads without the parts; 0071 only without the parts or without fixed rects |
| `0073-gpu-compositor-a-frame-without-its-parts-composes-th.patch` | `gfx/layers/composite/LayerManagerComposite.{h,cpp}`, `gfx/layers/ipc/CompositorBridgeParent.{h,cpp}` | T-505: a frame without its parts reads the changed rows whole, so its diff-driven partial composite covers them across the frame's width (`JihadWholeRows`; the columns beside a narrow change used to be the window target's last composite: the long page's wrong verify frame, 2026-11-02); the inspector's review fixes: one sample time per job, fixed rects joined again after the reduction, the parts' signature over a cut part's slice, every part's clip and mask, blend modes as covered; the parts pass under the job's cancel flag |
| `0054-gpu-compositor-kept-rects-all-saved-before-any-is-wr.patch` | `gfx/layers/opengl/CompositorOGL.cpp`, `widget/PuppetWidget.cpp`, `dom/canvas/WebGLContext.cpp`, `widget/headless/GfxInfo.cpp` | T-517/T-511 review: a window-moved shift saves every kept rect before writing any; `jihad_offscreen_gpu_given_up()` / `jihad_gpu_compositor_on()`, so WebGL makes no EGL contexts once the daemon has given the GPU path up |
| `0053-gpu-compositor-a-shift-that-is-the-daemon-s-window-m.patch` | `gfx/layers/opengl/CompositorOGL.{h,cpp}`, `widget/PuppetWidget.cpp` | T-517: a compose job's shift may be the daemon's move of the frame's window in its segment (flag 8): the rows are in place, only the kept rects are copied from where they were |
| `0052-gpu-compositor-the-frame-s-bands-read-as-one-column-.patch` | `widget/PuppetWidget.{h,cpp}`, `gfx/layers/ipc/CompositorBridgeParent.cpp`, `gfx/layers/opengl/CompositorOGL.{h,cpp}` | T-511: a compose job may give its bands' stale columns (flag 4); with no shift, kept rows or fixed columns, the bands are read as the one column the change and they span when it is under three quarters of the width; the poll reports the change's columns (twelve ints); `JIHAD_GPU_NO_COLUMN_READ=1` reads whole rows |
+54
View File
@@ -0,0 +1,54 @@
T-505, the long page's wrong verify frame (2026-11-02)
======================================================
The owed item from PICKUP 2026-10-26 (night), open item 2: one frame of 58 in pan-v1-long (2026-10-26) had 13 wrong
rows at frame rows 0..13 (slot 1, y 43, shift 184, own slot, bands 1040..1224).
Reproduced (pan-v2-long: JIHAD_GPU_VERIFY=1 JIHAD_GPU_VERIFY_DUMP_WRONG=1, "pan 0 3000 15000", main lane only, the
daemon f25ef5ba / engine 446b22790a of the device): 81 verified frames, 2 wrong, both slot 1:
- line 581: y -133, shift 8 from its y -141 frame, bands 1216..1224: 8 rows wrong at 133..141 = the base's header
(part rect 0,140..765,198, opaque rows 141..197) where the shift of 8 left its top rows, not put back. The dump
(vs-slot.raw / vs-whole.raw, looked at: rows 133..140 are the header's #2b4c7e in the frame, the page's #f8f6f0 in
the reference; 141..197 the header in both; the out-of-page rows above white in both).
- line 615: y 42, shift 175 from that y -133 frame: 14 rows wrong at 0..14, rows 14..23 right. Exactly the record
the first wrong frame left behind: its beneath (rows 140..198, captured after its restore failed) holds the old
header in its rows 1..49 and the page's background in 49..58; shifted by 175 those land at 0..14 (wrong) and
14..23 (right). So the restore WORKS for a base at y -133 and FAILED for the base at y -141 (the load's frame,
rows 0..141 above the page); the defect heals itself one frame later.
The same geometry in pan-v1-long (2026-10-26): slot 1's first shift (184) from its y -141 frame, 13 wrong rows at
0..13 = the header's bottom (184..197) not put back.
Not it (read): cpuPartsDress's restore arithmetic (d = frameY - srcY, rows outside the read bands), the own-slot
path's srcSlot/srcY (baseSlot = slot, baseY = the slot's y), the engine's frameY (set only with a src or a
diff-first shift), the published top= (the card's scroll, not the frame's y), the part rect's inflation (the
engine's Inflate(1): 58 rows, "the edges' filtered pixels"), the hide of the parts (CompositeToTarget's hook, both
paths).
The daemon instrumented (cpuPartsDress logs the base record, d, the read rows, the rows put back and a pixel of the
record's second row; the main poll logs the job's window against the slot's) and the round again (pan-v3-long, 84
verified frames, 2 wrong, slot 2 this time: shift 1 from y -141 -> 1 wrong row at 140; then shift 137 -> 56 wrong at
3..59). The log settles it (lines 435, 629, 670): the restore RAN ("58 rows put back"), and the base record's probe
read (100,1)=ff2b4c7e, the header, where the other slots' records read fff8f6f0, the page: the record was made bad at
its own dress (line 435), by a frame whose job was the load's tree change "changed 141..1083 x 764..768" (the 4 px
strip beside the scroll port), too tall for the band surface -> "the whole path".
ROOT CAUSE (the inspector's F1, found the same way from the code, context/impl/impl-review-findings-t505-cpuparts.md
review 2): LayerManagerComposite::UpdateAndRender makes a composite whose change is smaller than the frame a PARTIAL
one, clipped and cleared to the change's bounds (here x 764..768), while a frame without its parts reads the changed
rows WHOLE (review D4 of 0072 turned the column read off for it). The columns beside the change come from the window
target's previous composite: in a verify run the whole reference with the parts -> the header in the frame's rows
141..197 before the dress -> the dress records it as what lies beneath -> the first shift puts it back where the old
header landed. Frames with the engine's parts read the columns (T-511), so they never saw it; band-path reads compose
exactly what they read.
FIX (engine 0073, LayerManagerComposite::JihadWholeRows, set by the job's content-only scope): a frame without its
parts has its diff-driven partial composite cover the changed rows across the frame's width, and reports them so.
With it, the inspector's F2-F6 (one sample time per job; overlapping part rects re-merged in the engine and every
beneath recorded before any part is drawn in the daemon; the main lane's second job only on a base of its kind; the
signature's misses; the parts pass under the job's cancel flag).
Frames looked at: pan-v2-long-frame-60.jpg (the header at the view's top, paragraphs 16-33 in order, the coloured
ones at 20, 25, 30) and the vs-slot/vs-whole crop (the header 8 rows taller in the frame).
The round with the fixes (pan-v4-long: engine 0073 (libxul 673bb742) + the daemon (3eb1e911) on the Enyo variant, the
same settings): 85 verified frames, 78 shifted, 0 wrong rows, 0 structurally wrong, no dump; the load's strip
composite now "changed 141..1083 x 0..768" (the rows whole); every base record's probe the page's pixels (59 the
background fff8f6f0, the rest paragraph colours and text; 6 with no base), none the header's. Frame 60 looked at
(the header at the view's top, the paragraphs in order). Earlier rounds on the same page, same settings: pan-v2 2 wrong
of 81, pan-v3 2 wrong of 84. (A verify run is no rate measurement: ~5 fps with the reference composites.)
Kept: pan-v2-long-frame-60.jpg, pan-v4-long-frame-60.jpg; the other pictures of the three rounds deleted.
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+37 -6
View File
@@ -2034,22 +2034,40 @@ __attribute__((optimize("O2"))) int BrowserPageGoanna::cpuPartsDress(int s, unsi
for (int k = 0; k < nf; k++)
if (p->wh[k][0] != fr[k][2] - fr[k][0] || p->wh[k][1] != fr[k][3] - fr[k][1]) return kPartsNoPic;
// what lay beneath the old frame's parts, where they landed, outside the rows read
int putBack = 0, baseN = 0, baseR[4] = { 0, 0, 0, 0 };
uint32_t baseProbe = 0;
long d = 0;
size_t baseBytes = 0;
if (src >= 0) {
const SlotParts& sp = mSlotParts[src];
if (sp.kind != 1) return kPartsNoBase;
const long d = (long)(cr.frameYValid ? cr.frameY : jobY) - srcY;
d = (long)(cr.frameYValid ? cr.frameY : jobY) - srcY;
baseN = sp.n; baseBytes = sp.beneath.size();
if (sp.n > 0) for (int c = 0; c < 4; c++) baseR[c] = sp.r[0][c];
if (sp.n > 0 && sp.beneath.size() >= (size_t)(sp.r[0][2] - sp.r[0][0]) * 4 * 2) // a pixel of the record's second row (what lay beneath the part's top)
memcpy(&baseProbe, sp.beneath.data() + (size_t)(sp.r[0][2] - sp.r[0][0]) * 4 + 100 * 4, 4);
auto readRow = [&](long y) { return (y >= cr.readLo && y < cr.readHi) || (y >= cr.read2Lo && y < cr.read2Hi); };
for (int k = 0; k < sp.n; k++) {
for (int k = sp.n - 1; k >= 0; k--) { // (review F3) the first recorded last: where rects overlap, its record is the oldest
const int x0 = sp.r[k][0], x1 = sp.r[k][2], rw = x1 - x0;
for (int y = sp.r[k][1]; y < sp.r[k][3]; y++) {
const long ny = y - d;
if (ny < 0 || ny >= H || readRow(ny)) continue;
memcpy(pixels + ((size_t)ny * w + x0) * 4, sp.beneath.data() + sp.off[k] + (size_t)(y - sp.r[k][1]) * rw * 4, (size_t)rw * 4);
putBack++;
}
}
}
{
// the restore, frame by frame (the long page's first shifted frame kept the base's header where it landed, 2026-11-02)
static uint32_t sDressN = 0;
if (sDressN++ < 80 || (sDressN % 50) == 0)
JIHAD_GPU_LOG("[jihad-gpu] cpu parts: slot %d (y %ld, frameY %ld/%d) dressed on slot %d (y %ld; %d parts, first %d,%d..%d,%d, %zu B beneath, (100,1)=%08x): d %ld, read %ld..%ld %ld..%ld, %d rows put back; its own %d parts, first %d,%d..%d,%d\n",
s, (long)jobY, (long)cr.frameY, (int)cr.frameYValid, src, srcY, baseN, baseR[0], baseR[1], baseR[2], baseR[3], baseBytes, baseProbe, d,
cr.readLo, cr.readHi, cr.read2Lo, cr.read2Hi, putBack, nf, nf > 0 ? fr[0][0] : 0, nf > 0 ? fr[0][1] : 0, nf > 0 ? fr[0][2] : 0, nf > 0 ? fr[0][3] : 0);
}
if (p) const_cast<PartsPic*>(p)->used = ++mPicClock;
// this frame's: what lies beneath kept, the parts drawn over it
// this frame's: what lies beneath kept (every part's before any is drawn: where two rects overlap, the second's
// record would hold the first's drawn pixels, review F3), then the parts drawn over it
SlotParts& dp = mSlotParts[s];
dp.beneath.resize(need);
size_t at = 0;
@@ -2057,9 +2075,13 @@ __attribute__((optimize("O2"))) int BrowserPageGoanna::cpuPartsDress(int s, unsi
const int x0 = fr[i][0], rw = fr[i][2] - x0;
dp.off[i] = at;
for (int c = 0; c < 4; c++) dp.r[i][c] = fr[i][c];
for (int y = fr[i][1]; y < fr[i][3]; y++, at += (size_t)rw * 4) {
for (int y = fr[i][1]; y < fr[i][3]; y++, at += (size_t)rw * 4)
memcpy(dp.beneath.data() + at, pixels + ((size_t)y * w + x0) * 4, (size_t)rw * 4);
}
for (int i = 0; i < nf; i++) {
const int x0 = fr[i][0], rw = fr[i][2] - x0;
for (int y = fr[i][1]; y < fr[i][3]; y++) {
uint32_t* row = (uint32_t*)(pixels + ((size_t)y * w + x0) * 4);
memcpy(dp.beneath.data() + at, row, (size_t)rw * 4);
const uint32_t* pp = (const uint32_t*)(p->px.data() + p->off[i]) + (size_t)(y - fr[i][1]) * rw;
for (int x = 0; x < rw; x++) {
const uint32_t sv = pp[x], a = sv >> 24;
@@ -4839,7 +4861,9 @@ bool BrowserPageGoanna::gpuJobStart(int slot, int key, unsigned char* pixels, si
static const bool sFixedCols = !getenv("JIHAD_GPU_NO_FIXED_COLS") && !getenv("JIHAD_GPU_READ_THREAD");
// (T-505: not without the parts: the job in flight's frame gets its parts on this thread when it is polled, while the
// compositor thread may be copying it for this one)
if (!co && !sWhole && mGpuNextSecond && mGpuNextBaseKey && mGpuNextBaseKey != key && mGpuFixedHi >= mGpuFixedLo) {
// (review F4) and not on a frame without its parts either: the parts turned off between the two posts (a cover
// refusal, the document's three misses) would copy a frame this thread dresses at its poll
if (!co && !sWhole && mGpuNextSecond && mGpuNextBaseKey && mGpuNextBaseKey != key && mGpuFixedHi >= mGpuFixedLo && !mGpuNextBaseContentOnly) {
// round 130: the job in flight is the base (its frame is complete when this job runs)
const long dy = (long)y - mGpuNextBaseY;
unsigned char* bb = (dy > -h && dy < h) ? attachShm(mGpuNextBaseKey) : nullptr;
@@ -6564,6 +6588,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
if (r == -3 && (!sStillOnly || (oldest.shift == 0 && gpuLo == oldest.y && !mGpuCompositorScroll))) {
mGpuNextBaseKey = oldest.key;
mGpuNextBaseWin = oldest.win; // T-517
mGpuNextBaseContentOnly = oldest.contentOnly; // (review F4)
mGpuNextBaseY = cr.frameYValid ? (int)cr.frameY : oldest.y;
// the composed job's changed rows are in ITS frame; the next job is at bandY: the hull
// of both readings (scrolled and fixed content), as the stale rule does
@@ -6659,6 +6684,12 @@ void BrowserPageGoanna::paintToSharedBuffer() {
if (ab > 0) memset(pixels, 0xff, (size_t)ab * w * 4);
if (pe < h && pe >= 0) memset(pixels + (size_t)pe * w * 4, 0xff, (size_t)(h - pe) * w * 4);
}
{
static uint32_t sPollN = 0;
if (sPollN++ < 80 || (sPollN % 50) == 0)
JIHAD_GPU_LOG("[jihad-gpu] cpu parts: poll slot %d (key 0x%x) job y %d win %d (the slot's %d), base slot %d y %ld\n",
gslot, (unsigned)oldest.key, oldest.y, oldest.win, gslot >= 0 ? mGpuSlotWin[gslot] : -1, oldest.srcSlot, oldest.srcY);
}
const int outcome = cpuPartsDress(gslot, pixels, cr, h, w, oldest.srcSlot, oldest.srcY, oldest.y, partsBufOf(oldest.token));
if (outcome == kPartsDressed) {
gpuPartsCpu = true;
+1
View File
@@ -525,6 +525,7 @@ private:
// the second job runs, the compositor thread runs them in order), exact while scrolling
int mGpuNextBaseKey = 0, mGpuNextBaseY = 0;
int mGpuNextBaseWin = 0; // T-517: that frame's window
bool mGpuNextBaseContentOnly = false; // T-505 (review F4): that frame without its parts (dressed on this thread at its poll)
void gpuJobDrain(); // wait out a frame in flight (its buffer is about to go away)
void gpuSlotsReset() {
gpuJobDrain();
+1 -1