diff --git a/build/desktop/build-goanna.sh b/build/desktop/build-goanna.sh index 76a4be4..b8cd65c 100755 --- a/build/desktop/build-goanna.sh +++ b/build/desktop/build-goanna.sh @@ -53,11 +53,14 @@ cd "$SRC" # Each step's failure stops the script (T-387, 2026-10-18): `./mach configure && ./mach build` hid a # failed configure from set -e (a non-final command of an && list), and the script went on to # install our components into the OLD dist and report "done" over an engine that never built. +# $2: parallel jobs for mach build (default: the mozconfig's), as build-goanna-arm.sh takes: one engine build at a +# time on this host, fewer jobs when memory is short (2026-11-03) +JOBS=${2:+-j$2} case "$STAGE" in configure) ./mach configure || { echo "ERROR: mach configure failed"; exit 1; } ;; - build) ./mach build || { echo "ERROR: mach build failed"; exit 1; } ;; + build) ./mach build $JOBS || { echo "ERROR: mach build failed"; exit 1; } ;; all) ./mach configure || { echo "ERROR: mach configure failed"; exit 1; } - ./mach build || { echo "ERROR: mach build failed"; exit 1; } ;; + ./mach build $JOBS || { echo "ERROR: mach build failed"; exit 1; } ;; *) echo "unknown stage: $STAGE (use configure|build|all)"; exit 2 ;; esac diff --git a/build/desktop/patches/0073-gpu-compositor-the-parts-picture-by-part-each-rect-i.patch b/build/desktop/patches/0073-gpu-compositor-the-parts-picture-by-part-each-rect-i.patch new file mode 100644 index 0000000..beac5b9 --- /dev/null +++ b/build/desktop/patches/0073-gpu-compositor-the-parts-picture-by-part-each-rect-i.patch @@ -0,0 +1,631 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NotAlexNoyle +Date: Tue, 3 Nov 2026 11:36:15 -0500 +Subject: [PATCH] =?UTF-8?q?gpu=20compositor:=20the=20parts'=20picture=20by?= + =?UTF-8?q?=20part=20=E2=80=94=20each=20rect=20its=20own=20signature,=20a?= + =?UTF-8?q?=20pass=20reads=20only=20the=20rects=20the=20daemon=20lacks?= +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +T-505 (the owner's "cheaper part pictures", 2026-11-03). A frame without its parts (flag 128) used to carry +one signature of all its parts; when it moved, the parts pass composed and read every part. Now each rect of +the parts list has a signature of its own: the outermost fixed or sticky layer's subtree as before, every +subtree layer's last paint as the stamp of one counter that only goes up (CompositableTransactionParent stamps +each compositable update's layer, part or not: a layer that becomes a part later carries it — review F2), the +part's scale and the sub-pixel phase of its position, where it sits inside the rect and its size (review F3: a +part wider than the frame moving sideways, or moving inside a merged rect, keeps the rect's size), and the +rect's size; a rect no part's rect meets (a nested part's own) takes every part's with its index and place +(review F6). The whole signature is the rects' and the global paint count, so a daemon with the one signature +behaves as before (every part read when any moved). + +jihad_offscreen_compose_post3/start3 (jihad_offscreen_overview_ok answers 4) take the list of signatures the +daemon holds pictures of; the pass packs and reads only the rects whose signature is not in it, in rect order, +and the poll's 54 ints carry each rect's signature (49..52) and the rects read as a mask (53). post2/start2 +keep their meaning. + +Measured by finger on desktop Wikipedia (its two sidebars; the contents panel, 310 of 340 KB, is the one that +changes): the pass reads the panel alone, 10.4 -> 11.4 fps flowing; the long page unchanged. Verify 0 wrong +rows over the scripted pans on both (docs/rounds/t505-2026-11-03/notes.txt). + +Co-Authored-By: Claude Fable 5.1 +--- + .../ipc/CompositableTransactionParent.cpp | 10 +- + gfx/layers/ipc/CompositorBridgeParent.cpp | 268 ++++++++++++------ + gfx/layers/ipc/CompositorBridgeParent.h | 7 +- + gfx/layers/opengl/CompositorOGL.h | 7 + + widget/PuppetWidget.cpp | 61 +++- + widget/PuppetWidget.h | 9 +- + 6 files changed, 264 insertions(+), 98 deletions(-) + +diff --git a/gfx/layers/ipc/CompositableTransactionParent.cpp b/gfx/layers/ipc/CompositableTransactionParent.cpp +index 6d6edb0043..356209d997 100644 +--- a/gfx/layers/ipc/CompositableTransactionParent.cpp ++++ b/gfx/layers/ipc/CompositableTransactionParent.cpp +@@ -31,6 +31,7 @@ namespace mozilla { + namespace layers { + + extern std::atomic gJihadPartsGen; // Jihad T-505, CompositorBridgeParent.cpp ++void JihadNotePartPaint(Layer* aLayer); // Jihad T-505 (per part): that layer painted (its paint stamp) + void JihadReqNote(const char* aSrc); // Jihad T-505 diagnostic, CompositorBridgeParent.cpp + + class ClientTiledLayerBuffer; +@@ -75,8 +76,13 @@ CompositableParentManager::ReceiveCompositableUpdate(const CompositableOperation + } + // Jihad T-505: an update to a layer under a fixed or sticky one moves the parts' generation (the daemon composes + // the page's fixed and sticky parts alone again when it moves: they changed) +- for (Layer* l = compositable->GetLayer(); l; l = l->GetParent()) { +- if (l->GetIsFixedPosition() || l->GetIsStickyPosition()) { ++gJihadPartsGen; break; } ++ { ++ // (per part, review F2) every layer's paint is stamped, part or not: a layer that becomes a part later carries it ++ Layer* painted = compositable->GetLayer(); ++ if (painted) JihadNotePartPaint(painted); ++ for (Layer* l = painted; l; l = l->GetParent()) { ++ if (l->GetIsFixedPosition() || l->GetIsStickyPosition()) { ++gJihadPartsGen; break; } ++ } + } + + switch (aEdit.detail().type()) { +diff --git a/gfx/layers/ipc/CompositorBridgeParent.cpp b/gfx/layers/ipc/CompositorBridgeParent.cpp +index 9aa08d7f9d..345efbadf9 100644 +--- a/gfx/layers/ipc/CompositorBridgeParent.cpp ++++ b/gfx/layers/ipc/CompositorBridgeParent.cpp +@@ -8,6 +8,7 @@ + #include // Jihad T-509: sleep() for JIHAD_GPU_TEST_STALL + #include // for uint64_t + #include // for _Rb_tree_iterator, etc ++#include // Jihad T-505 (per part): sJihadPartPaintLock + #include // for pair + #include "LayerTransactionParent.h" // for LayerTransactionParent + #include "RenderTrace.h" // for RenderTraceLayers +@@ -30,6 +31,20 @@ static bool sJihadContentOnly = false; // Jihad T-505: the job is a frame withou + // sticky layer counts as a part, none of the fixed rows' handling applies + namespace mozilla { namespace layers { + std::atomic gJihadPartsGen(0); // Jihad T-505: updates to layers under a fixed or sticky one ++// Jihad T-505 (per part): every layer's last paint, as the stamp of ONE counter that only goes up (review F2: a count ++// per part that started again after a clear could return to a value a held picture was signed with; a layer painted ++// while it was no part keeps its stamp for when it becomes one). CompositableTransactionParent notes each update; an ++// ImageBridge update may come on its own thread. Past 4096 layers the map is cleared: the stamps lost make every ++// part's signature differ once (a pass), and the next paints take new stamps, so a lost stamp never matches ++static std::mutex sJihadPaintStampLock; ++static uint32_t sJihadPaintStamp = 0; ++static std::map sJihadLayerPaint; ++void JihadNotePartPaint(Layer* aLayer) ++{ ++ std::lock_guard g(sJihadPaintStampLock); ++ if (sJihadLayerPaint.size() > 4096) sJihadLayerPaint.clear(); ++ sJihadLayerPaint[aLayer] = ++sJihadPaintStamp; ++} + } } // (CompositableTransactionParent.cpp) + #include // Jihad T-502: floor, ceil + #ifdef MOZ_WIDGET_GTK +@@ -1167,87 +1182,152 @@ CompositorBridgeParent::JihadHideForJob() + }); + } + +-// T-505 (E2): FNV-1a over what makes the parts' pixels: each layer in a fixed or sticky subtree (its identity and +-// type, opacity, content flags, mask, visible region; inside a part its transform and clip too, so an inner scroll or a +-// move inside the part counts; a colour layer's colour; an animation: every composite), the paints of layers under a +-// part (gJihadPartsGen) and the parts' sizes on the frame (a part crossing the frame's edge is new pixels). A content +-// 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 ++// T-505 (E2, per part): FNV-1a per outermost fixed or sticky layer over what makes its pixels: each layer of its subtree ++// (identity and type, opacity, content flags, mask, visible region; inside the part its transform and clip too, so an ++// inner scroll or a move inside the part counts; a colour layer's colour; an animation: every composite), the paints ++// under it (sJihadPartPaint) and, cut by the frame's top or bottom edge, the slice shown. Each rect of aRects takes the ++// signatures of the parts whose rects meet it and its own size (aRectSig; a rect no part's rect meets, a nested ++// part's own, takes every part's). The whole is the rects' signatures, their count and the global paint count ++// (gJihadPartsGen: a daemon with the one signature reads every part when any paint happened under one, as before). ++// A content 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, int aWidth, int aHeight) ++CompositorBridgeParent::JihadPartsSig(const gfx::IntRect* aRects, int aN, bool* aCovered, int aWidth, int aHeight, uint32_t* aRectSig) + { +- uint32_t h = 2166136261u; ++ const uint32_t kBasis = 2166136261u; ++ uint32_t h = kBasis; // the part being walked + auto mix = [&](uint32_t v) { h = (h ^ v) * 16777619u; }; + auto mixf = [&](float f) { uint32_t u; memcpy(&u, &f, sizeof u); mix(u); }; + auto mixr = [&](const gfx::IntRect& r) { mix(r.x); mix(r.y); mix(r.width); mix(r.height); }; +- mix((uint32_t)gJihadPartsGen); +- for (int i = 0; i < aN; i++) { mix(aRects[i].width); mix(aRects[i].height); } ++ struct TopPart { uint32_t sig; gfx::IntRect ir; gfx::IntRect tb; }; // tb: its bounds on the frame, not inflated ++ TopPart tops[32]; ++ int nTop = 0; ++ gfx::IntRect topIr, topTb; ++ std::map stamps; // the layers' paint stamps, taken once under the lock ++ { ++ std::lock_guard g(sJihadPaintStampLock); ++ stamps = sJihadLayerPaint; ++ } ++ auto stamp = [&](Layer* l) { auto it = stamps.find(l); return it != stamps.end() ? it->second : 0u; }; ++ const gfx::IntRect frame(0, 0, aWidth, aHeight); + *aCovered = false; + Layer* root = mLayerManager ? mLayerManager->GetRoot() : nullptr; +- if (!root) return h; +- static uint32_t sAnimTick = 0; +- static uint32_t sCoverLog = 0; +- int depth = 0; +- nsIntRegion partsSoFar; +- auto onFrame = [&](Layer* x, nsIntRegion& aOut) { +- gfx::Matrix m; +- if (!x->GetEffectiveTransform().Is2D(&m) || !m.PreservesAxisAlignedRectangles()) { +- const gfx::IntRect vb = x->GetLocalVisibleRegion().GetBounds().ToUnknownRect(); +- aOut.OrWith(gfx::RoundedOut(x->GetEffectiveTransform().TransformBounds(gfx::Rect(vb.x, vb.y, vb.width, vb.height)))); +- return; +- } +- const nsIntRegion visible = x->GetLocalVisibleRegion().ToUnknownRegion(); +- for (auto it = visible.RectIter(); !it.Done(); it.Next()) { +- const gfx::IntRect& q = it.Get(); +- aOut.OrWith(gfx::RoundedOut(x->GetEffectiveTransform().TransformBounds(gfx::Rect(q.x, q.y, q.width, q.height)))); +- } +- }; +- ForEachNode(root, +- [&](Layer* l) { +- 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()); +- 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->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(l)->GetColor(); +- mixf(c.r); mixf(c.g); mixf(c.b); mixf(c.a); +- } +- if (!l->GetAnimations().IsEmpty()) mix(++sAnimTick); +- if (part && depth == 0) onFrame(l, partsSoFar); +- } else if (!l->GetFirstChild() && !partsSoFar.IsEmpty() && l->GetEffectiveOpacity() > 0.0f) { +- const Layer::LayerType t = l->GetType(); +- if (t == Layer::TYPE_PAINTED || t == Layer::TYPE_COLOR || t == Layer::TYPE_IMAGE || t == Layer::TYPE_CANVAS) { +- nsIntRegion mine; +- onFrame(l, mine); +- mine.AndWith(partsSoFar); +- if (!mine.IsEmpty()) { +- if (!*aCovered && sCoverLog++ < 20) { +- const gfx::IntRect b = mine.GetBounds(); +- JIHAD_GPU_LOG("[jihad-gpu] parts: layer %p (type %d) drawn above a part at %d,%d %dx%d\n", l, (int)t, b.x, b.y, b.width, b.height); ++ if (root) { ++ static uint32_t sAnimTick = 0; ++ static uint32_t sCoverLog = 0; ++ int depth = 0; ++ nsIntRegion partsSoFar; ++ auto onFrame = [&](Layer* x, nsIntRegion& aOut) { ++ gfx::Matrix m; ++ if (!x->GetEffectiveTransform().Is2D(&m) || !m.PreservesAxisAlignedRectangles()) { ++ const gfx::IntRect vb = x->GetLocalVisibleRegion().GetBounds().ToUnknownRect(); ++ aOut.OrWith(gfx::RoundedOut(x->GetEffectiveTransform().TransformBounds(gfx::Rect(vb.x, vb.y, vb.width, vb.height)))); ++ return; ++ } ++ const nsIntRegion visible = x->GetLocalVisibleRegion().ToUnknownRegion(); ++ for (auto it = visible.RectIter(); !it.Done(); it.Next()) { ++ const gfx::IntRect& q = it.Get(); ++ aOut.OrWith(gfx::RoundedOut(x->GetEffectiveTransform().TransformBounds(gfx::Rect(q.x, q.y, q.width, q.height)))); ++ } ++ }; ++ ForEachNode(root, ++ [&](Layer* l) { ++ const bool part = l->GetIsFixedPosition() || l->GetIsStickyPosition(); ++ if (part || depth > 0) { ++ if (part && depth == 0) { ++ // a new part: its own hash; its rect on the frame as JihadFixedRectsNow makes it, and its bounds; (review F3) ++ // its scale and the sub-pixel phase of its position (a quarter pixel): a picture made at another zoom ++ // or phase is other pixels ++ h = kBasis; ++ topIr = topTb = gfx::IntRect(); ++ const gfx::IntRect vb = l->GetLocalVisibleRegion().GetBounds().ToUnknownRect(); ++ if (!vb.IsEmpty()) { ++ topTb = gfx::RoundedOut(l->GetEffectiveTransform().TransformBounds(gfx::Rect(vb.x, vb.y, vb.width, vb.height))); ++ topIr = topTb; ++ topIr.Inflate(1); ++ topIr = topIr.Intersect(frame); ++ } ++ gfx::Matrix m2; ++ if (l->GetEffectiveTransform().Is2D(&m2)) { ++ mixf(m2._11); mixf(m2._22); mixf(m2._12); mixf(m2._21); ++ mix((uint32_t)lroundf((m2._31 - floorf(m2._31)) * 4.0f)); mix((uint32_t)lroundf((m2._32 - floorf(m2._32)) * 4.0f)); ++ } else { ++ mix(0x3d3d3d3du); ++ } ++ } ++ mix((uint32_t)(uintptr_t)l); mix((uint32_t)l->GetType()); mixf(l->GetOpacity()); ++ mix(stamp(l)); // its last paint (every layer of the subtree: review F2) ++ 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->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(l)->GetColor(); ++ mixf(c.r); mixf(c.g); mixf(c.b); mixf(c.a); ++ } ++ if (!l->GetAnimations().IsEmpty()) mix(++sAnimTick); ++ if (part && depth == 0) onFrame(l, partsSoFar); ++ } else if (!l->GetFirstChild() && !partsSoFar.IsEmpty() && l->GetEffectiveOpacity() > 0.0f) { ++ const Layer::LayerType t = l->GetType(); ++ if (t == Layer::TYPE_PAINTED || t == Layer::TYPE_COLOR || t == Layer::TYPE_IMAGE || t == Layer::TYPE_CANVAS) { ++ nsIntRegion mine; ++ onFrame(l, mine); ++ mine.AndWith(partsSoFar); ++ if (!mine.IsEmpty()) { ++ if (!*aCovered && sCoverLog++ < 20) { ++ const gfx::IntRect b = mine.GetBounds(); ++ JIHAD_GPU_LOG("[jihad-gpu] parts: layer %p (type %d) drawn above a part at %d,%d %dx%d\n", l, (int)t, b.x, b.y, b.width, b.height); ++ } ++ *aCovered = true; + } +- *aCovered = true; + } + } +- } +- if (part) depth++; +- }, +- [&](Layer* l) { if (l->GetIsFixedPosition() || l->GetIsStickyPosition()) depth--; }); +- return h; ++ if (part) depth++; ++ }, ++ [&](Layer* l) { ++ if (!(l->GetIsFixedPosition() || l->GetIsStickyPosition())) return; ++ if (--depth > 0) return; ++ // the part walked: its signature, rect and bounds kept (past 32 parts, folded into the last) ++ if (nTop < 32) { tops[nTop].sig = h; tops[nTop].ir = topIr; tops[nTop].tb = topTb; nTop++; } ++ else { tops[31].sig = (tops[31].sig ^ h) * 16777619u; tops[31].ir = tops[31].ir.Union(topIr); tops[31].tb = tops[31].tb.Union(topTb); } ++ }); ++ } ++ // each rect's signature, and the whole ++ uint32_t whole = kBasis; ++ auto mixw = [&](uint32_t v) { whole = (whole ^ v) * 16777619u; }; ++ mixw((uint32_t)gJihadPartsGen); mixw((uint32_t)aN); ++ for (int k = 0; k < aN; k++) { ++ h = kBasis; ++ mix((uint32_t)aRects[k].width); mix((uint32_t)aRects[k].height); ++ bool any = false; ++ for (int i = 0; i < nTop; i++) { ++ if (tops[i].ir.IsEmpty() || !aRects[k].Intersects(tops[i].ir)) continue; ++ mix(tops[i].sig); any = true; ++ // (review F3) where the part sits inside the rect, and its size: a part wider than the frame moving sideways ++ // under a cut rect, a part moving inside a merged rect, keep the rect's size and would keep its picture ++ mix((uint32_t)(tops[i].tb.x - aRects[k].x)); mix((uint32_t)(tops[i].tb.y - aRects[k].y)); ++ mix((uint32_t)tops[i].tb.width); mix((uint32_t)tops[i].tb.height); ++ } ++ if (!any) { // (review F6) a rect no part's rect meets: every part's, and its own index and place ++ for (int i = 0; i < nTop; i++) mix(tops[i].sig); ++ mix((uint32_t)k); mix((uint32_t)aRects[k].x); mix((uint32_t)aRects[k].y); ++ } ++ if (aRectSig) aRectSig[k] = h; ++ mixw(h); ++ } ++ return whole; + } + + void +@@ -1483,11 +1563,12 @@ JihadFixedCols() + + // T-544: the frame's fixed and stuck rects (the last JihadUpdateLastFixed's) out to the daemon + static void +-JihadStuckOut(JihadFrameRequest* aReq, const gfx::IntRect* aRects, int aN) ++JihadStuckOut(JihadFrameRequest* aReq, const gfx::IntRect* aRects, int aN, const uint32_t* aSig = nullptr) + { + aReq->nStuck = 0; + for (int k = 0; k < aN && aReq->nStuck < 4; k++) { + if (aRects[k].IsEmpty()) continue; ++ aReq->partsSig[aReq->nStuck] = aSig ? aSig[k] : 0; // T-505 (per part): the rect's own signature + int* o = aReq->stuck[aReq->nStuck++]; + o[0] = aRects[k].x; o[1] = aRects[k].y; o[2] = aRects[k].XMost(); o[3] = aRects[k].YMost(); + } +@@ -2032,7 +2113,8 @@ 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, aWidth, aHeight); ++ uint32_t rectSig[16] = {}; ++ aReq->partsGen = JihadPartsSig(parts, partN, &covered, aWidth, aHeight, rectSig); + if (covered) aReq->partsFlags |= 1; + { + sJihadContentOnly = false; // the card's hold set keeps T-544's meaning: fixed layers and stuck ones +@@ -2047,11 +2129,25 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal + } + } + mLayerManager->JihadEndTransactionNoComposite(); +- JihadStuckOut(aReq, parts, partN); +- if (!covered && aReq->partsDest && aReq->nStuck > 0 && aReq->partsGen != aReq->partsHave) { ++ JihadStuckOut(aReq, parts, partN, rectSig); ++ // T-505 (per part): the rects to read — with the list of signatures the daemon holds, the ones not in it; with ++ // the one signature, every rect when the whole differs ++ int want = 0; ++ if (!covered && aReq->partsDest && aReq->nStuck > 0) { ++ if (aReq->nPartsHave >= 0) { ++ for (int k = 0; k < aReq->nStuck; k++) { ++ bool have = false; ++ for (int j = 0; j < aReq->nPartsHave && j < 16 && !have; j++) have = aReq->partsHaveList[j] == aReq->partsSig[k]; ++ if (!have) want |= 1 << k; ++ } ++ } else if (aReq->partsGen != aReq->partsHave) { ++ want = (1 << aReq->nStuck) - 1; ++ } ++ } ++ if (want) { + size_t need = 0; + for (int k = 0; k < aReq->nStuck; k++) +- need += (size_t)(aReq->stuck[k][2] - aReq->stuck[k][0]) * (aReq->stuck[k][3] - aReq->stuck[k][1]) * 4; ++ if (want & (1 << k)) need += (size_t)(aReq->stuck[k][2] - aReq->stuck[k][0]) * (aReq->stuck[k][3] - aReq->stuck[k][1]) * 4; + // the rects cut into pieces no taller than the surface, on shelves across its width (12 regions at most) + struct Piece { int k, x, y, w, h, atX, atY; } pieces[12]; + int np = 0, used = 0; +@@ -2059,6 +2155,7 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal + np = 0; used = 0; + int shelfY = 0, shelfH = 0, cursorX = 0; + for (int k = 0; k < aReq->nStuck; k++) { ++ if (!(want & (1 << k))) continue; // a rect the daemon has + const int x0 = aReq->stuck[k][0], w = aReq->stuck[k][2] - x0; + for (int y = aReq->stuck[k][1]; y < aReq->stuck[k][3];) { + const int hgt = std::min(aCap, aReq->stuck[k][3] - y); +@@ -2105,9 +2202,11 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal + mLayerManager->JihadSetRegions(nullptr, nullptr, 0); + const TimeStamp pr0 = TimeStamp::Now(); + bool ok = true; +- size_t off[4] = {}; +- 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; ++ size_t off[4] = {}; // each read rect's place in the buffer: the read ones packed in rect order ++ for (int k = 0, o = 0; k < aReq->nStuck; k++) { ++ off[k] = (size_t)o; ++ if (want & (1 << k)) o += (aReq->stuck[k][2] - aReq->stuck[k][0]) * (aReq->stuck[k][3] - aReq->stuck[k][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]; +@@ -2117,12 +2216,15 @@ CompositorBridgeParent::JihadComposeAndRead(JihadFrameRequest* aReq, float aScal + ogl->JihadEndBandPass(); + ogl->JihadSetClearTransparent(false); + sJihadOverview = 0; +- if (ok) aReq->partsBytes = (int)need; ++ if (ok) { aReq->partsBytes = (int)need; aReq->partsRead = want; } + static uint32_t sPassLog = 0; +- if (sPassLog++ < 20 || (sPassLog % 50) == 0) +- JIHAD_GPU_LOG("[jihad-gpu] parts pass: %d parts, %d pieces on %d rows, %zu KB: compose %.1f ms, read %.1f ms%s\n", +- aReq->nStuck, np, used, need / 1024, (pr0 - pc0).ToMilliseconds(), (TimeStamp::Now() - pr0).ToMilliseconds(), ++ if (sPassLog++ < 20 || (sPassLog % 50) == 0) { ++ int nRead = 0; ++ for (int k = 0; k < aReq->nStuck; k++) if (want & (1 << k)) nRead++; ++ JIHAD_GPU_LOG("[jihad-gpu] parts pass: %d of %d parts (mask %x), %d pieces on %d rows, %zu KB: compose %.1f ms, read %.1f ms%s\n", ++ nRead, aReq->nStuck, want, np, used, need / 1024, (pr0 - pc0).ToMilliseconds(), (TimeStamp::Now() - pr0).ToMilliseconds(), + ok ? "" : " (failed)"); ++ } + } + mLayerManager->JihadIgnoreDiff(false); + mLayerManager->JihadSetWorldTransform(world); +diff --git a/gfx/layers/ipc/CompositorBridgeParent.h b/gfx/layers/ipc/CompositorBridgeParent.h +index ce9fa9ccf2..e3d0ef657d 100644 +--- a/gfx/layers/ipc/CompositorBridgeParent.h ++++ b/gfx/layers/ipc/CompositorBridgeParent.h +@@ -546,9 +546,10 @@ public: + gfx::IntRect mJihadLastKeep[4], mJihadPrevKeep[4]; + int mJihadPrevKeepN = 0; + 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, int aWidth, int aHeight); ++ // Jihad T-505 (E2): the parts' signature (their layers, content, attributes and sizes), and each rect's own ++ // (aRectSig, aN of them: per part); *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, int aWidth, int aHeight, uint32_t* aRectSig); + int JihadFixedRectsNow(int aWidth, int aHeight, gfx::IntRect* aOut, gfx::IntRect* aKeep); + void JihadUpdateLastFixed(int aWidth, int aHeight); + bool mJihadSeqKnown = false; +diff --git a/gfx/layers/opengl/CompositorOGL.h b/gfx/layers/opengl/CompositorOGL.h +index f33ebe4908..2af33a0568 100644 +--- a/gfx/layers/opengl/CompositorOGL.h ++++ b/gfx/layers/opengl/CompositorOGL.h +@@ -185,6 +185,13 @@ struct JihadFrameRequest { + uint8_t* partsDest = nullptr; + int partsCap = 0; + uint32_t partsHave = 0; ++ // T-505 (per part, 2026-11-03): each rect of stuck[] has a signature of its own (partsSig[k]: the parts it holds, ++ // their paints and its size). A job posted with the list of signatures the daemon holds pictures of (partsHaveList, ++ // nPartsHave >= 0: compose_post3/start3) reads only the rects whose signature is not in it, packed in rect order; ++ // partsRead says which (bit k). One posted with the one signature (post2/start2, nPartsHave -1) reads every rect ++ // when the whole differs, as before ++ uint32_t partsHaveList[16] = {}; int nPartsHave = -1; ++ uint32_t partsSig[4] = {}; int partsRead = 0; // out + int partsBytes = 0; // out: the bytes the job's parts pass wrote (0: no pass) + int partsFlags = 0; // out: 1 a content layer above a part (the frame cannot be dressed), 2 too big + int hold[4][4] = {}; // out: the card's hold set (x0, y0, x1, y1) of a frame without its parts +diff --git a/widget/PuppetWidget.cpp b/widget/PuppetWidget.cpp +index 5a45a2f1f2..9507c010e5 100644 +--- a/widget/PuppetWidget.cpp ++++ b/widget/PuppetWidget.cpp +@@ -853,6 +853,7 @@ struct JihadReadJob { + bool overviewParts = false; // T-504 (flag 64): the overview's fixed and stuck parts alone + bool contentOnly = false; // T-505 (flag 128): the frame without its fixed and sticky parts + uint8_t* partsDest = nullptr; int partsCap = 0; uint32_t partsHave = 0; // T-505 (E2): its parts pass ++ uint32_t partsHaveList[16] = {}; int nPartsHave = -1; // T-505 (per part): the signatures the daemon holds (post3/start3) + int invLo, invHi; // the rows the composite changed + float scrollX = 0.0f, scrollY = 0.0f; bool scrollValid = false; // the card's scroll for the compositor + uint32_t token; +@@ -905,6 +906,8 @@ void JihadReadOnCompositorThread(JihadReadJob* job) + job->req.overviewParts = job->overviewParts; + job->req.contentOnly = job->contentOnly; + job->req.partsDest = job->partsDest; job->req.partsCap = job->partsCap; job->req.partsHave = job->partsHave; ++ job->req.nPartsHave = job->nPartsHave; ++ for (int i = 0; i < 16; i++) job->req.partsHaveList[i] = job->partsHaveList[i]; + job->req.scrollX = job->scrollX; job->req.scrollY = job->scrollY; job->req.scrollValid = job->scrollValid; + job->req.done = &JihadJobDone; job->req.cookie = job; + // Once done() has run the daemon may forget (delete) the job at any moment (round 108's wake +@@ -941,7 +944,8 @@ uint32_t + PuppetWidget::JihadComposeStart(nsIDocShell* aDocShell, void* aDest, int aStride, + int aWidth, int aHeight, int aRowLo, int aRowHi, int aRow2Lo, int aRow2Hi, int aShift, + const void* aSrc, int aSrcY, int aFlags, int aColX0, int aColX1, +- void* aPartsDest, int aPartsCap, uint32_t aPartsHave) ++ void* aPartsDest, int aPartsCap, uint32_t aPartsHave, ++ const uint32_t* aPartsHaveList, int aNPartsHave) + { + if (!aDocShell || !aDest || aWidth <= 0 || aHeight <= 0 || aStride < aWidth * 4) { + return 0; +@@ -950,7 +954,7 @@ PuppetWidget::JihadComposeStart(nsIDocShell* aDocShell, void* aDest, int aStride + return 0; + } + return JihadPostJob(aDest, aStride, aWidth, aHeight, aRowLo, aRowHi, aRow2Lo, aRow2Hi, aShift, aSrc, aSrcY, aFlags, +- nullptr, false, aColX0, aColX1, aPartsDest, aPartsCap, aPartsHave); ++ nullptr, false, aColX0, aColX1, aPartsDest, aPartsCap, aPartsHave, aPartsHaveList, aNPartsHave); + } + + bool +@@ -1042,7 +1046,8 @@ uint32_t + PuppetWidget::JihadPostJob(void* aDest, int aStride, int aWidth, int aHeight, int aRowLo, int aRowHi, int aRow2Lo, + int aRow2Hi, int aShift, const void* aSrc, int aSrcY, int aFlags, + const float* aView, bool aScrollValid, int aColX0, int aColX1, +- void* aPartsDest, int aPartsCap, uint32_t aPartsHave) ++ void* aPartsDest, int aPartsCap, uint32_t aPartsHave, ++ const uint32_t* aPartsHaveList, int aNPartsHave) + { + mozilla::layers::CompositorBridgeParent* bridge = JihadBridge(); + MessageLoop* loop = mozilla::layers::CompositorThreadHolder::Loop(); +@@ -1069,6 +1074,10 @@ PuppetWidget::JihadPostJob(void* aDest, int aStride, int aWidth, int aHeight, in + job->contentOnly = (aFlags & 128) != 0; // T-505 + if (job->contentOnly && aPartsDest && aPartsCap > 0) { // T-505 (E2) + job->partsDest = static_cast(aPartsDest); job->partsCap = aPartsCap; job->partsHave = aPartsHave; ++ if (aPartsHaveList && aNPartsHave >= 0) { // (per part) the signatures the daemon holds pictures of ++ job->nPartsHave = std::min(aNPartsHave, 16); ++ for (int i = 0; i < job->nPartsHave; i++) job->partsHaveList[i] = aPartsHaveList[i]; ++ } + } + if (job->colsKnown) { + job->colX0 = std::max(0, std::min(aColX0, aWidth)); job->colX1 = std::max(0, std::min(aColX1, aWidth)); +@@ -1083,13 +1092,14 @@ uint32_t + PuppetWidget::JihadComposePost(void* aDest, int aStride, int aWidth, int aHeight, int aRowLo, int aRowHi, + int aRow2Lo, int aRow2Hi, int aShift, const void* aSrc, int aSrcY, int aFlags, + const float* aView, bool aScrollValid, int aColX0, int aColX1, +- void* aPartsDest, int aPartsCap, uint32_t aPartsHave) ++ void* aPartsDest, int aPartsCap, uint32_t aPartsHave, ++ const uint32_t* aPartsHaveList, int aNPartsHave) + { + if (!aDest || aWidth <= 0 || aHeight <= 0 || aStride < aWidth * 4) { + return 0; + } + return JihadPostJob(aDest, aStride, aWidth, aHeight, aRowLo, aRowHi, aRow2Lo, aRow2Hi, aShift, aSrc, aSrcY, aFlags, +- aView, aScrollValid, aColX0, aColX1, aPartsDest, aPartsCap, aPartsHave); ++ aView, aScrollValid, aColX0, aColX1, aPartsDest, aPartsCap, aPartsHave, aPartsHaveList, aNPartsHave); + } + + int +@@ -1158,6 +1168,10 @@ PuppetWidget::JihadComposePoll(uint32_t aToken, bool aWait, int* aOut, bool* aSc + for (int k = 0; k < 4; k++) + for (int c = 0; c < 4; c++) aOut[33 + 4 * k + c] = k < job->req.nHold ? job->req.hold[k][c] : 0; + } ++ if (aOutLen >= 54) { // T-505 (per part): each rect's signature, and the rects the pass read (bit k) ++ for (int k = 0; k < 4; k++) aOut[49 + k] = (int)job->req.partsSig[k]; ++ aOut[53] = job->req.partsRead; ++ } + } + if (aScrollOnly) *aScrollOnly = job->req.scrollOnly; + } else if (!aWait && job->req.composed) { +@@ -1535,8 +1549,9 @@ MOZ_EXPORT int + jihad_offscreen_overview_ok() + { + static const bool sPass = getenv("JIHAD_GPU_PASS") != nullptr; +- return sPass ? 0 : 3; // 2 (T-505): flag 128 too, and the poll's 30th int; 3 (E2): compose_post2/start2's parts +- // pass and the poll's 49 ints ++ return sPass ? 0 : 4; // 2 (T-505): flag 128 too, and the poll's 30th int; 3 (E2): compose_post2/start2's parts ++ // pass and the poll's 49 ints; 4 (per part): post3/start3 with the signatures the daemon ++ // holds, the poll's 54 ints (each rect's signature, the rects read) + } + MOZ_EXPORT int + jihad_offscreen_compose_peek(nsIWidget* aWidget, uint32_t aToken) +@@ -1608,6 +1623,38 @@ jihad_offscreen_compose_start2(nsIWidget* aWidget, nsIDocShell* aDocShell, void* + (aEngineShift & 4) ? aColX0 : 0, (aEngineShift & 4) ? aColX1 : 0, + aPartsDest, aPartsCap, aPartsHave); + } ++// T-505 (per part): post and start with the list of signatures the daemon holds part pictures of (aNHave of them, 16 at ++// most; 0 is a list: every part read): the pass reads only the rects whose signature is not in it ++MOZ_EXPORT uint32_t ++jihad_offscreen_compose_post3(nsIWidget* aWidget, void* aDest, int aStride, int aWidth, int aHeight, ++ int aRowLo, int aRowHi, int aRow2Lo, int aRow2Hi, int aShift, ++ const void* aSrc, int aSrcY, int aFlags, int aUseView, double aScale, ++ double aOffX, double aOffY, double aScrollX, double aScrollY, int aScrollValid, ++ int aColX0, int aColX1, void* aPartsDest, int aPartsCap, const uint32_t* aHave, int aNHave) ++{ ++ if (!aWidget) { ++ return 0; ++ } ++ const float view[5] = { (float)aScale, (float)aOffX, (float)aOffY, (float)aScrollX, (float)aScrollY }; ++ return static_cast(aWidget)->JihadComposePost(aDest, aStride, aWidth, aHeight, aRowLo, aRowHi, ++ aRow2Lo, aRow2Hi, aShift, aSrc, aSrcY, aFlags, ++ aUseView ? view : nullptr, aScrollValid != 0, ++ (aFlags & 4) ? aColX0 : 0, (aFlags & 4) ? aColX1 : 0, ++ aPartsDest, aPartsCap, 0, aHave, aHave ? std::max(aNHave, 0) : -1); ++} ++MOZ_EXPORT uint32_t ++jihad_offscreen_compose_start3(nsIWidget* aWidget, nsIDocShell* aDocShell, void* aDest, int aStride, ++ int aWidth, int aHeight, int aRowLo, int aRowHi, int aRow2Lo, int aRow2Hi, int aShift, ++ const void* aSrc, int aSrcY, int aEngineShift, int aColX0, int aColX1, ++ void* aPartsDest, int aPartsCap, const uint32_t* aHave, int aNHave) ++{ ++ if (!aWidget) { ++ return 0; ++ } ++ return static_cast(aWidget)->JihadComposeStart(aDocShell, aDest, aStride, aWidth, aHeight, aRowLo, aRowHi, aRow2Lo, aRow2Hi, aShift, aSrc, aSrcY, aEngineShift, ++ (aEngineShift & 4) ? aColX0 : 0, (aEngineShift & 4) ? aColX1 : 0, ++ aPartsDest, aPartsCap, 0, aHave, aHave ? std::max(aNHave, 0) : -1); ++} + MOZ_EXPORT int + jihad_offscreen_compose_poll(nsIWidget* aWidget, uint32_t aToken, int* aOut, int* aScrollOnly) + { +diff --git a/widget/PuppetWidget.h b/widget/PuppetWidget.h +index 5378b5afa0..5d30174715 100644 +--- a/widget/PuppetWidget.h ++++ b/widget/PuppetWidget.h +@@ -293,7 +293,8 @@ public: + int aWidth, int aHeight, int aRowLo, int aRowHi, int aRow2Lo = 0, int aRow2Hi = 0, int aShift = 0, + const void* aSrc = nullptr, int aSrcY = 0, int aFlags = 0, // aFlags: 1 engine shift, 2 verify, 4 columns + int aColX0 = 0, int aColX1 = 0, // T-511: the bands' stale columns (flag 4) +- void* aPartsDest = nullptr, int aPartsCap = 0, uint32_t aPartsHave = 0); // T-505 (E2) ++ void* aPartsDest = nullptr, int aPartsCap = 0, uint32_t aPartsHave = 0, // T-505 (E2) ++ const uint32_t* aPartsHaveList = nullptr, int aNPartsHave = -1); // T-505 (per part) + // aOut (10 ints, may be null): the rows the tree changed, the rows fixed layers cover, the two + // bands read, the frame's position and its valid flag; 12 for a job posted with flag 4 (T-511): + // the columns the composite changed; aScrollOnly: the composite's only change was the card's scroll +@@ -307,7 +308,8 @@ public: + uint32_t JihadComposePost(void* aDest, int aStride, int aWidth, int aHeight, int aRowLo, int aRowHi, + int aRow2Lo, int aRow2Hi, int aShift, const void* aSrc, int aSrcY, int aFlags, + const float* aView, bool aScrollValid, int aColX0 = 0, int aColX1 = 0, +- void* aPartsDest = nullptr, int aPartsCap = 0, uint32_t aPartsHave = 0); // T-505 (E2) ++ void* aPartsDest = nullptr, int aPartsCap = 0, uint32_t aPartsHave = 0, // T-505 (E2) ++ const uint32_t* aPartsHaveList = nullptr, int aNPartsHave = -1); // T-505 (per part) + void JihadComposeForget(uint32_t aToken); + // T-515: a job's state without taking its result: 1 done, 2 composed (being read), 0 pending, -1 unknown + int JihadComposePeek(uint32_t aToken); +@@ -471,7 +473,8 @@ private: + uint32_t JihadPostJob(void* aDest, int aStride, int aWidth, int aHeight, int aRowLo, int aRowHi, int aRow2Lo, + int aRow2Hi, int aShift, const void* aSrc, int aSrcY, int aFlags, + const float* aView = nullptr, bool aScrollValid = false, int aColX0 = 0, int aColX1 = 0, +- void* aPartsDest = nullptr, int aPartsCap = 0, uint32_t aPartsHave = 0); ++ void* aPartsDest = nullptr, int aPartsCap = 0, uint32_t aPartsHave = 0, ++ const uint32_t* aPartsHaveList = nullptr, int aNPartsHave = -1); // T-505 (per part) + uint32_t mJihadNextToken = 1; + float mJihadScrollX = 0.0f, mJihadScrollY = 0.0f; // T-502: the card's scroll for the compositor + bool mJihadScrollValid = false; diff --git a/build/engine/engine-version.env b/build/engine/engine-version.env index 375b82c..c03dc62 100644 --- a/build/engine/engine-version.env +++ b/build/engine/engine-version.env @@ -9,7 +9,7 @@ UXP_BASE=6b18be917d462369593bfe611230a7003fd7d945 # The tip of our engine branch (NotAlexNoyle/UXP-Jihad, jihad/uxp-: 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=52ad9ab66ebbc1c28133a330ee2f35819565d61e +UXP_PIN=adfaa7ff3054c6076a9e6fddc85677f081f8841b # What the user agent and the start pages say: Goanna version and the short UPSTREAM base. GOANNA_VERSION=6.9 UXP_SHORT=6b18be9 diff --git a/build/webos-oe/recipes-jihad/goanna/goanna_1.0.bb b/build/webos-oe/recipes-jihad/goanna/goanna_1.0.bb index ffb3d01..3ae1448 100644 --- a/build/webos-oe/recipes-jihad/goanna/goanna_1.0.bb +++ b/build/webos-oe/recipes-jihad/goanna/goanna_1.0.bb @@ -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=" SRC_URI = "git://${JIHAD_REPO}/third_party/uxp;protocol=file;nobranch=1" -SRCREV = "52ad9ab66ebbc1c28133a330ee2f35819565d61e" +SRCREV = "adfaa7ff3054c6076a9e6fddc85677f081f8841b" S = "${WORKDIR}/git" # Modern C++14 toolchain (device-build R1) — the stock dylan/device gcc cannot build UXP. diff --git a/context/impl/impl-review-findings-t505-cpuparts.md b/context/impl/impl-review-findings-t505-cpuparts.md index 715316b..748660a 100644 --- a/context/impl/impl-review-findings-t505-cpuparts.md +++ b/context/impl/impl-review-findings-t505-cpuparts.md @@ -73,3 +73,26 @@ 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). + +## Review 3 — the inspector on the parts by part and the express second frame (2026-11-03; Codex out of credits until Nov 4th) + +The review gate for T-505's item 3 (engine commit `jihad/uxp-RB_20261027`'s per-part commit, the daemon's diff of the +same day): ONE ck:inspector, read-only. Verdict: REVISE (P1 3, P2 1, P3 3). Every finding fixed the same day, the +engine commit amended before it entered the queue. + +| # | sev | where | finding | fix | status | +|---|---|---|---|---|---| +| F1 | P1 | daemon xStart | the two-deep restore covers two records, but after `mXJob = mXJob2` the second frame becomes the first and a third builds on it: its copy may hold the first's base's parts (caught in the second's raw copy before the second's restore), which two records do not reach | no second frame on a first that is itself two deep (`aBase->twoDeep`): the chain is two frames long | fixed; v-pe rounds 0 wrong | +| F2 | P1 | engine sJihadPartPaint | a per-part paint COUNT cleared at 64 entries could return to a value a held picture was signed with (the comment claimed the opposite); paints of a layer before it became a part, or while nested under another part, were not its own | every layer's last paint is the stamp of ONE counter that only goes up, stamped for every compositable update (part or not); each subtree layer's stamp is mixed into its part's signature; the map's clear at 4096 loses stamps, which only makes signatures differ | fixed | +| F3 | P1 (both modes, older than this change) | engine JihadPartsSig | the signature carried a part's size, not where it sits: a sticky header wider than the frame moving sideways under a rect at [0,w), a part moving inside a merged rect of constant size, a zoom rounding to the same rect size — all kept the old picture | each part meeting a rect mixes its position inside the rect and its size; a part's scale and the quarter-pixel phase of its translation are in its own signature | fixed; by reading (no such page in the test set) | +| F4 | P2 | the express verify | the reference's whole read sits between the first frame's read and the second's copy, so under verify the copy never catches the first's dress: the 55 two-deep frames verified did not exercise the race | `JIHAD_GPU_TEST_DRESS_HOLD=:` holds a first frame's dress before its restores or between its record and its draw; a verify round with it (notes) | fixed; the hold rounds in `docs/rounds/t505-2026-11-03/notes.txt` | +| F5 | P3 | daemon cpuPartsDress | storing a picture the job read could evict a picture this frame draws (the least used, bumped only after the store): a frame not shown and a miss counted | every picture of this frame's signatures is made the newest before the store | fixed | +| F6 | P3 | engine JihadPartsSig | two rects no part's rect meets, of one size, took the same signature: one picture for both | the fallback mixes the rect's index and place | fixed | +| F7 | P3 | daemon xPoll | the `!mPage` path dropped the job without releasing its verify buffer; an empty `else if (twoDeep)` branch | `xVerifyDrop` there; the branch gone | fixed | + +Checked and fine (the inspector): the pass's buffer layout and the daemon's reading of it (off[] over the wanted rects, +partsBytes against the read rects, a failed pass); the two-deep restore's shifts, order, own-slot case through +`mSlotPrev`, the serial lookup, no pixels written before a refusal; the failure paths; the express verify's guard +(baseline, request count, scroll, view, fixed rects, keeps, probe) and its buffer's release on every path; +the map's lock; older daemons' 29/49 ints; level 4's gate; post3/start3's argument order; the whole mode with +per-part pictures. diff --git a/context/kits/cavekit-gpu-rendering.md b/context/kits/cavekit-gpu-rendering.md index f9b08f1..e7e6bc8 100644 --- a/context/kits/cavekit-gpu-rendering.md +++ b/context/kits/cavekit-gpu-rendering.md @@ -58,7 +58,7 @@ Hacker News 36-37, desktop Wikipedia 13-14 (software 9-12.4). ### R3: Pans meet the chart, on real pages too **Description:** A drag-scroll moves the content by compositing the painted displayport, not by repainting. **Acceptance Criteria:** -- [ ] The scripted pan (0 → 3000 card px in 10 s, `tools/device/t502/pan-run.sh`) runs at 24 fps or better on `jihad-long-static.html` and on desktop en.wikipedia.org after its load, **with the card moving as under a finger** *(2026-10-26, RE-OPENED (the owner's choice): the scripted pan sent the card's scroll commands to the daemon and the card's view stood still, so every figure below left out the card's own redraw; it now tells the card to scroll and the card reports its scroll, as under a finger (`JIHAD_INJECT_PAN_NOCARD=1` is the old test). Measured so: desktop Wikipedia 91 frames in 10.3 s by the scripted pan, 86 in 11.9 s of eight real finger drags (~7 fps), the long page 222 in 11.6 s (~19); the readback waits behind LunaSysMgr's redraw of the card on the same GPU (a 15-row read 30-164 ms under a finger). The old test's figures follow.)* *(2026-10-26, T-527, MET with the old test on the owner's bench (sensord and atlas-sensord stopped: the owner's choice (b) of 2026-10-24), the packaged head: desktop Wikipedia 24.6 and 24.2 fps over the whole 10 s pan, the long page 34.2; with the sensors running the same build gives 22.7-23.6 and 32.8-33.1 (`docs/rounds/t527-2026-10-25/notes.txt`).)* *(2026-10-25, T-527, the sensors running, on engine 0063: the long page 29.5-35.4 fps; desktop Wikipedia's 2 s windows 22.3-28.5 after the first scroll's restyle, its mean over the pan 23.8 and 24.4 fps in two runs (21.7 in a third): the compositor thread is the bound (85 % of a core: compose ~8 ms, the copy from the base 10-14, the read 7-12 a frame). 2026-10-24, T-527, the owner's lever 3b: the bench's `sensord` takes ~30% of a core at idle (load 3.5). With it and `atlas-sensord` stopped the Wikipedia pan ran 25.0 / 27.5 / 27.4 fps against 21.4 / 20.9 / 22.4 with them running, the long page 33-36.5 against 30-35; both restored after. The floor is met on the bench without the sensors; with them it is 21-22, so the compositor thread stays the product's lever. 2026-10-19, T-515: the test page 30-35 fps; desktop Wikipedia 19-21 fps in 2 s windows, median frame gap 45 ms, with the frames made off the main thread. The page's script no longer gaps them (its 2.2 s restyle ran under frames every ~45 ms); the floor is the compositor thread's ~45 ms a Wikipedia frame, whichever lane asks for it: T-517. A page whose script holds the main thread 600 ms in every 700 pans at 26-32 fps (2.4-3.8 without the express lane).)* +- [ ] The scripted pan (0 → 3000 card px in 10 s, `tools/device/t502/pan-run.sh`) runs at 24 fps or better on `jihad-long-static.html` and on desktop en.wikipedia.org after its load, **with the card moving as under a finger** *(2026-10-26, RE-OPENED (the owner's choice): the scripted pan sent the card's scroll commands to the daemon and the card's view stood still, so every figure below left out the card's own redraw; it now tells the card to scroll and the card reports its scroll, as under a finger (`JIHAD_INJECT_PAN_NOCARD=1` is the old test). Measured so: desktop Wikipedia 91 frames in 10.3 s by the scripted pan, 86 in 11.9 s of eight real finger drags (~7 fps), the long page 222 in 11.6 s (~19); the readback waits behind LunaSysMgr's redraw of the card on the same GPU (a 15-row read 30-164 ms under a finger). The old test's figures follow.)* *(2026-10-26, T-527, MET with the old test on the owner's bench (sensord and atlas-sensord stopped: the owner's choice (b) of 2026-10-24), the packaged head: desktop Wikipedia 24.6 and 24.2 fps over the whole 10 s pan, the long page 34.2; with the sensors running the same build gives 22.7-23.6 and 32.8-33.1 (`docs/rounds/t527-2026-10-25/notes.txt`).)* *(2026-10-25, T-527, the sensors running, on engine 0063: the long page 29.5-35.4 fps; desktop Wikipedia's 2 s windows 22.3-28.5 after the first scroll's restyle, its mean over the pan 23.8 and 24.4 fps in two runs (21.7 in a third): the compositor thread is the bound (85 % of a core: compose ~8 ms, the copy from the base 10-14, the read 7-12 a frame). 2026-10-24, T-527, the owner's lever 3b: the bench's `sensord` takes ~30% of a core at idle (load 3.5). With it and `atlas-sensord` stopped the Wikipedia pan ran 25.0 / 27.5 / 27.4 fps against 21.4 / 20.9 / 22.4 with them running, the long page 33-36.5 against 30-35; both restored after. The floor is met on the bench without the sensors; with them it is 21-22, so the compositor thread stays the product's lever. 2026-10-19, T-515: the test page 30-35 fps; desktop Wikipedia 19-21 fps in 2 s windows, median frame gap 45 ms, with the frames made off the main thread. The page's script no longer gaps them (its 2.2 s restyle ran under frames every ~45 ms); the floor is the compositor thread's ~45 ms a Wikipedia frame, whichever lane asks for it: T-517. A page whose script holds the main thread 600 ms in every 700 pans at 26-32 fps (2.4-3.8 without the express lane).)* *(2026-11-03, T-505 item 3: the parts by part and the express second frame back — by finger 11.4-11.5 fps on Wikipedia, 18.0-18.3 on the long page: the compositor job and the card's own redraw bound it, not the parts pass (~5 % of the time) nor the lane's depth; item 4, the scroll-only bands composed on the CPU, is next.)* - [x] On every page measured, the GPU path's pan is at least as fast as the software path's on the same pan (`NOGPU=1`), and the table in `docs/RENDERING-OPTIONS.md` says so per page *(2026-10-24, T-519, the same 10 s pan on each page both ways on engine 0058, the pan's 2 s windows: long static 30.4-33.3 against 10.8-12.5 fps, fixed header and footer 26.4-27.9 against 10.9-12.7, sticky-mix 16.5-19.0 against 8.6-10.0, the animation page panned 14.5-24.4 against 11.4-23.6, the self-scrolling page 22.8-27.7 against 10.5-11.4, Hacker News 37.5-48.0 against 6.3-10.2 (the pan overshoots its 1249 px, the last frames are past the page), desktop Wikipedia 18.5-25.9 against 2.7-2.9 (the 18.5 window is the first scroll's restyle). The table is in `docs/RENDERING-OPTIONS.md`; logs `docs/rounds/t519-2026-10-24/pan-*-gpu2-*` and `pan-*-sw-*`.)* - [~] Frame gaps of 56 ms or more during a pan are accounted for (traced to their source) and the avoidable ones removed *(2026-10-26, T-527: on the owner's bench, desktop Wikipedia 22 and 16 gaps in 10 s: the express job itself 21 (a tree change's read, the copy from the base), a job queued behind the one before it 14, one tile-upload transaction, and once 415 ms with all three buffers out with the card; none waits on the main thread. Tried and not kept: a region read of the change, the own slot with stuck layers (notes.txt). What is left is the compositor job's cost on its one thread.)* *(2026-10-25, T-527: traced per gap with `tools/host/gap-trace.py` on the timeline (the express lane now notes why it made no frame): desktop Wikipedia's 10 s pan, 24 gaps in 252 frames — an express job of 50-100 ms (a frame whose tree changed: a whole read; the copy from the base with a stuck header) while the two-deep pipeline waits, a job queued 25-88 ms behind the page's tile-upload transaction, once all three buffers held by the card; none is the main thread any more. Removing them is the compositor job's floor (the read, the copy), still open. 2026-10-18, traced: desktop Wikipedia's pan has 3-13 a 2 s window. The stall sampler resolves them to the page's own restyle on the first scroll (955 ms, 36% selector matching: its sticky header), the layer paints of the displayport moves (58-121 ms: display list, glyphs, regions), and short misc events. None is the daemon's own work; the anchor distance does not change them (0.125 and 0.4 heights measured). What would remove them from the frames is the frames made off the main thread: T-515. 2026-10-19: done (T-515, the express lane): the main thread's stalls are covered by frames composed and published from a thread of their own; the gaps left are the compositor's own (T-517).)* - [x] During a pan the card has content past the view's edges, as the software path's frames have: a gap between frames or a busy main thread shows the page, not grey *(added 2026-10-18 from the owner's "areas turn grey": the GPU frame carries 0.15 of the card's height above and below the view (engine 0046, `JIHAD_GPU_OVERSCAN`); verify 0 structurally wrong rows with it on the test page, sticky-mix, desktop Wikipedia and Hacker News; frame dumps looked at)* @@ -73,6 +73,7 @@ Hacker News 36-37, desktop Wikipedia 13-14 (software 9-12.4). - [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.)* - [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)* +- [x] The parts' pictures by part (T-505, the owner's item 3): every rect of a frame's parts list carries a signature of its own (its parts' layers, the paints under them, its size) and a pass composes and reads only the rects the daemon holds no picture of; the express lane's second frame builds on the first in flight when both are without their parts, its dress putting back what lay beneath the first's parts and beneath the first's base's; `JIHAD_GPU_VERIFY=1` checks the express lane's frames as the main lane's; 0 wrong rows over the scripted pans on desktop Wikipedia and the long page in both lanes *(added and met 2026-11-03: engine 0073 (per-rect signatures, `compose_post3`/`start3`, the poll's 54 ints) and the daemon's pictures by part, each behind a switch — `JIHAD_GPU_PARTS_PER=1`, `JIHAD_GPU_EXPRESS_SECOND_PARTS=1` — the defaults the owner's call. By finger on the build with the review's fixes: desktop Wikipedia 10.8 → 11.4 fps with the parts by part, 11.4 with the second frame (its two sidebars; the contents panel that changes is 310 of the 340 KB, so the lever is bounded by the changing part's size), the long page 19.2 → 19.4 / 18.1 (one part, no pass during the drags). Verify after the pan mark on that build: the main lane alone 55 and 52 (Wikipedia) / 80 (the long page) frames; both lanes with the second frame posted early (`JIHAD_GPU_EXPRESS_EARLY=1`) 35 / 66 frames with 13 / 29 two-deep ones, and with the first frame's dress held 80 ms before its restores or between its record and its draw (`JIHAD_GPU_TEST_DRESS_HOLD`, so the copy catches the dress) 34 / 55 and 34 / 61 frames with 13-16 / 24-27 two-deep ones — 0 wrong rows in restored or copied rows in every run (two single rows in rows those jobs read fresh at the load's end); frames looked at. On the way a latent main-lane fault from 2026-11-02 found and fixed: a second job whose in-flight base the parts rule refused built on the newest finished frame but told the engine it was the previous composite, so the frame was recorded 316 rows off and the next own-slot frames copied from the wrong rows (5 wrong frames in one run; 0 after the fix). `docs/rounds/t505-2026-11-03/notes.txt`; the review: one inspector (Codex out of credits), `context/impl/impl-review-findings-t505-cpuparts.md` review 3.)* **Dependencies:** R2 ### R5: Animation and new content meet the chart diff --git a/context/plans/build-site.md b/context/plans/build-site.md index 227062b..9cc2d93 100644 --- a/context/plans/build-site.md +++ b/context/plans/build-site.md @@ -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-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, after the engine update to RB_20261027 (the owner's order of 2026-11-02): 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, `/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 uploads, deferred uploads, a topped-up pool and the express lane on its own slot measured and not kept; the band frame is GPU-bound (r357). BLOCKED by the circuit breaker: the owner's finger round (T-514) and the bench's sensord load (the owner's call) decide) | +| 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-03 (item 3 done: the parts' pictures by part — engine 0073: each rect of the parts list signed on its own (its parts' subtree, the paints under its outermost layer, its size), `compose_post3`/`start3` with the signatures the daemon holds, the pass reads only the rects it lacks, the poll's 54 ints; the daemon keeps eight pictures by part — and the express lane's second frame on the first in flight back (review C1) under a double restore (the first's beneath and its base's, by slot record serial); each behind a switch (`JIHAD_GPU_PARTS_PER=1`, `JIHAD_GPU_EXPRESS_SECOND_PARTS=1`; the defaults the owner's call); `JIHAD_GPU_VERIFY=1` verifies the express lane's frames too. By finger (the build with the review's fixes) desktop Wikipedia 10.8 → 11.4 / 11.4 fps (the contents panel, 310 of 340 KB, is the part that changes: the lever is bounded by it), the long page 19.2 / 19.4 / 18.1; verify after the pan mark 0 wrong rows in both lanes, 142 two-deep frames among them, 55-80 with the first frame's dress held so the copy catches it (`docs/rounds/t505-2026-11-03/notes.txt`); the review gate one inspector (Codex out until Nov 4th): REVISE, seven findings, all fixed (`impl-review-findings-t505-cpuparts.md` review 3); a latent main-lane fault from review 2's F4 (a second job's wrong previous-composite flag, the frame recorded 316 rows off) found by the verify and fixed. Next: item 4, the scroll-only bands composed on the CPU from the tiles) — earlier: 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, after the engine update to RB_20261027 (the owner's order of 2026-11-02): 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, `/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 uploads, deferred uploads, a topped-up pool and the express lane on its own slot measured and not kept; the band frame is GPU-bound (r357). BLOCKED by the circuit breaker: the owner's finger round (T-514) and the bench's sensord load (the owner's call) decide) | | 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`; the inject channel `thumb `) | | T-508 | Video and Flash on the GPU path: `