T-515 (part): frames made off the main thread, the express lane (cavekit-gpu-rendering R3, R6)

With JIHAD_GPU_EXPRESS=1 on the GPU path each card's YAP commands are read on a thread of the
proxy's own and the daemon's frame bookkeeping is under one frame lock, which the main thread lets go
of only in the engine's pump and in a frame's layer paint. An express thread composes the layer tree
as it stands at the card's newest scroll and zoom and publishes the frame: every frame while the card
moves (the main lane paints layers and moves the anchor), and the main thread's stalls otherwise.
Engine 0048 splits the frame's paint from its compose post and lets any thread post and poll jobs;
0049 stops a composite acknowledged after the card's widget is gone from dereferencing it (a crash
on a card's disconnect seen in a device round).

Device rounds 240-269: zooms while the page's script holds the main thread come within 250 ms
(Wikipedia after its load 10/10 within 160-225 ms, held off median 681 and three never framed; a
page holding the main thread 600 ms in every 700 within 45-91 ms, held off median 450); that page
pans at 26-32 fps (2.4-3.8). Desktop Wikipedia pans at 19-21 fps: the compositor thread's ~45 ms a
frame is the floor now (T-517); the double taps' slow frames are GPU draws (T-516). Off by default
with the GPU path; no change with it off (the test page 30-33 fps, software 2.9-3.2).

Also: wants under a lock of their own (the reader thread never waits for the frame lock: a proxy's
teardown joins it), commands tagged in arrival order (a main frame serves the card once it took up
the newest), main frames older than an express publish dropped, the zoom hold released by an
express frame, the inject channel's pan and zoom through the card's path, JIHAD_GPU_TIMELINE,
JIHAD_GPU_ZOOM_DUMP=log, zoom-seq.sh, jihad-script-hold.html; a 32-bit 1L << 40 in the band merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J67ZH8aYy8uzhgZDZ6Nvx9
This commit is contained in:
NotAlexNoyleandClaude Opus 5.5 committed 2026-10-19 03:53:01 -04:00
1 parent 51bda89012
commit 5f7c9fc300
26 files changed
+1742 -40

No files matched your search

@@ -0,0 +1,324 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: NotAlexNoyle <alexnoyle@icloud.com>
Date: Mon, 19 Oct 2026 03:21:16 -0400
Subject: [PATCH] gpu compositor: the frame's paint and its compose posted
apart; compose jobs from any thread
T-515, frames made off the main thread. JihadComposeStart was the layer paint (main thread) and the
compose job's post in one call. They are now apart: jihad_offscreen_paint_layers(widget, docshell)
flushes and paints the layers, as Start does first; jihad_offscreen_compose_post(...) posts the
compose and read of the layer tree as it stands, from any thread, at a view and scroll it is given
(or the ones last set). The daemon's express lane posts its frames from a thread of its own while
the main thread runs the page's script or a layer paint.
The job list is shared with that thread, so it is under a mutex: start, post, forget and abandon
take it; a poll without a wait holds it for the whole poll (a forget between the find and the
job's monitor would let the compositor task free the job under it). jihad_offscreen_compose_peek
reads a job's state without taking its result (1 done, 2 composed and being read, 0 pending).
A forgotten job's cancelled flag was set even when the job was done; it is set only with abandoned.
---
widget/PuppetWidget.cpp | 145 ++++++++++++++++++++++++++++++++++------
widget/PuppetWidget.h | 16 +++++
2 files changed, 142 insertions(+), 19 deletions(-)
diff --git a/widget/PuppetWidget.cpp b/widget/PuppetWidget.cpp
index 02f93eedc7..179589978a 100644
--- a/widget/PuppetWidget.cpp
+++ b/widget/PuppetWidget.cpp
@@ -4,6 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <unistd.h> // Jihad T-502: write() to the done fd
+#include "mozilla/Maybe.h" // Jihad T-515
#include "base/basictypes.h"
#include "ClientLayerManager.h"
@@ -911,30 +912,40 @@ 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)
{
- const bool aEngineShift = (aFlags & 1) != 0;
if (!aDocShell || !aDest || aWidth <= 0 || aHeight <= 0 || aStride < aWidth * 4) {
return 0;
}
+ if (!JihadPaintLayers(aDocShell)) {
+ return 0;
+ }
+ return JihadPostJob(aDest, aStride, aWidth, aHeight, aRowLo, aRowHi, aRow2Lo, aRow2Hi, aShift, aSrc, aSrcY, aFlags);
+}
+
+bool
+PuppetWidget::JihadPaintLayers(nsIDocShell* aDocShell)
+{
+ if (!aDocShell) {
+ return false;
+ }
nsCOMPtr<nsIPresShell> presShell = aDocShell->GetPresShell();
if (!presShell) {
- return 0;
+ return false;
}
presShell->FlushPendingNotifications(Flush_Layout);
nsViewManager* vm = presShell->GetViewManager();
nsView* root = vm ? vm->GetRootView() : nullptr;
nsIWidget* viewWidget = root ? root->GetWidget() : nullptr;
if (!viewWidget) {
- return 0;
+ return false;
}
LayerManager* lm = viewWidget->GetLayerManager();
ClientLayerManager* clm = lm ? lm->AsClientLayerManager() : nullptr;
mozilla::layers::CompositorBridgeParent* bridge = JihadBridge();
if (!clm || !bridge) {
- return 0;
+ return false;
}
- MessageLoop* loop = mozilla::layers::CompositorThreadHolder::Loop();
- if (!loop) {
- return 0;
+ if (!mozilla::layers::CompositorThreadHolder::Loop()) {
+ return false;
}
mozilla::TimeStamp t0 = mozilla::TimeStamp::Now();
nsRegion dirty(root->GetDimensions());
@@ -942,8 +953,7 @@ PuppetWidget::JihadComposeStart(nsIDocShell* aDocShell, void* aDest, int aStride
mozilla::TimeStamp t1 = mozilla::TimeStamp::Now();
static uint32_t sFrames = 0;
if (sFrames < 20 || (sFrames % 50) == 0) {
- fprintf(stderr, "[jihad-gpu] frame %u: layers %.1f ms, posted (rows %d..%d asked)\n",
- sFrames, (t1 - t0).ToMilliseconds(), aRowLo, aRowHi);
+ fprintf(stderr, "[jihad-gpu] frame %u: layers %.1f ms\n", sFrames, (t1 - t0).ToMilliseconds());
}
if (sFrames == 5 || (sFrames % 200) == 0) {
// T-502 device rounds: every flag that throttles the page's JavaScript (timers to a second,
@@ -962,31 +972,84 @@ PuppetWidget::JihadComposeStart(nsIDocShell* aDocShell, void* aDest, int aStride
win && win->IsBackground() ? "BACKGROUND" : "foreground");
}
++sFrames;
+ return true;
+}
+
+// T-515: a job posted to the compositor thread, from the main thread (after its paint) or from the
+// express lane (no paint); the list is shared, so under its lock
+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)
+{
+ mozilla::layers::CompositorBridgeParent* bridge = JihadBridge();
+ MessageLoop* loop = mozilla::layers::CompositorThreadHolder::Loop();
+ if (!bridge || !loop || !aDest) {
+ return 0;
+ }
+ mozilla::MutexAutoLock listLock(mJihadJobsLock);
uint32_t token = mJihadNextToken++;
if (mJihadNextToken == 0) {
mJihadNextToken = 1;
}
JihadReadJob* job = new JihadReadJob(bridge, static_cast<uint8_t*>(aDest), aStride, aWidth, aHeight,
- mJihadScale, mJihadOffX, mJihadOffY, aRowLo, aRowHi, token);
- job->scrollX = mJihadScrollX; job->scrollY = mJihadScrollY; job->scrollValid = mJihadScrollValid;
- job->row2Lo = aRow2Lo; job->row2Hi = aRow2Hi; job->shift = aShift; job->src = static_cast<const uint8_t*>(aSrc); job->srcY = aSrcY; job->engineShift = aEngineShift; job->verify = (aFlags & 2) != 0;
+ aView ? (aView[0] > 0.0f ? aView[0] : 1.0f) : mJihadScale,
+ aView ? aView[1] : mJihadOffX, aView ? aView[2] : mJihadOffY, aRowLo, aRowHi, token);
+ job->scrollX = aView ? aView[3] : mJihadScrollX; job->scrollY = aView ? aView[4] : mJihadScrollY;
+ job->scrollValid = aView ? aScrollValid : mJihadScrollValid;
+ job->row2Lo = aRow2Lo; job->row2Hi = aRow2Hi; job->shift = aShift; job->src = static_cast<const uint8_t*>(aSrc); job->srcY = aSrcY;
+ job->engineShift = (aFlags & 1) != 0; job->verify = (aFlags & 2) != 0;
job->posted = mozilla::TimeStamp::Now();
mJihadJobs.push_back(job);
loop->PostTask(NewRunnableFunction(&JihadReadOnCompositorThread, job));
return token;
}
+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)
+{
+ 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);
+}
+
int
PuppetWidget::JihadComposePoll(uint32_t aToken, bool aWait, int* aOut, bool* aScrollOnly)
{
+ // T-515: the job list is shared with the express lane. Without a wait the whole poll holds the
+ // list's lock: a forget from another thread between the find and the job's monitor would let the
+ // compositor task free the job under it. A wait (the synchronous compose, the main thread's own
+ // token) releases it while waiting.
+ mozilla::Maybe<mozilla::MutexAutoLock> heldLock;
+ if (!aWait) {
+ heldLock.emplace(mJihadJobsLock);
+ }
JihadReadJob* job = nullptr;
- size_t at = 0;
- for (size_t i = 0; i < mJihadJobs.size(); ++i) {
- if (mJihadJobs[i]->token == aToken) { job = mJihadJobs[i]; at = i; break; }
+ {
+ mozilla::Maybe<mozilla::MutexAutoLock> findLock;
+ if (aWait) {
+ findLock.emplace(mJihadJobsLock);
+ }
+ for (size_t i = 0; i < mJihadJobs.size(); ++i) {
+ if (mJihadJobs[i]->token == aToken) { job = mJihadJobs[i]; break; }
+ }
}
if (!job) {
return -1;
}
+ auto removeJob = [&]() {
+ mozilla::Maybe<mozilla::MutexAutoLock> eraseLock;
+ if (aWait) {
+ eraseLock.emplace(mJihadJobsLock);
+ }
+ for (size_t i = 0; i < mJihadJobs.size(); ++i) {
+ if (mJihadJobs[i] == job) { mJihadJobs.erase(mJihadJobs.begin() + i); break; }
+ }
+ };
bool finished = false, ok = false;
{
mozilla::MonitorAutoLock lock(job->mon);
@@ -1020,20 +1083,36 @@ PuppetWidget::JihadComposePoll(uint32_t aToken, bool aWait, int* aOut, bool* aSc
}
}
if (finished) {
- mJihadJobs.erase(mJihadJobs.begin() + at);
+ removeJob();
delete job;
return ok ? 1 : -1;
}
if (aWait) {
- mJihadJobs.erase(mJihadJobs.begin() + at); // abandoned: the task frees it
+ removeJob(); // abandoned: the task frees it
return -1;
}
return 0;
}
+int
+PuppetWidget::JihadComposePeek(uint32_t aToken)
+{
+ mozilla::MutexAutoLock listLock(mJihadJobsLock);
+ for (size_t i = 0; i < mJihadJobs.size(); ++i) {
+ JihadReadJob* job = mJihadJobs[i];
+ if (job->token != aToken) {
+ continue;
+ }
+ mozilla::MonitorAutoLock lock(job->mon);
+ return job->done ? 1 : (job->req.composed ? 2 : 0);
+ }
+ return -1;
+}
+
void
PuppetWidget::JihadComposeForget(uint32_t aToken)
{
+ mozilla::MutexAutoLock listLock(mJihadJobsLock); // T-515
for (size_t i = 0; i < mJihadJobs.size(); ++i) {
JihadReadJob* job = mJihadJobs[i];
if (job->token != aToken) {
@@ -1043,7 +1122,7 @@ PuppetWidget::JihadComposeForget(uint32_t aToken)
{
mozilla::MonitorAutoLock lock(job->mon);
done = job->done;
- if (!done) job->abandoned = true; job->req.cancelled = true;
+ if (!done) { job->abandoned = true; job->req.cancelled = true; }
}
mJihadJobs.erase(mJihadJobs.begin() + i);
if (done) delete job;
@@ -1054,13 +1133,14 @@ PuppetWidget::JihadComposeForget(uint32_t aToken)
void
PuppetWidget::JihadComposeAbandonAll()
{
+ mozilla::MutexAutoLock listLock(mJihadJobsLock); // T-515
for (size_t i = 0; i < mJihadJobs.size(); ++i) {
JihadReadJob* job = mJihadJobs[i];
bool done;
{
mozilla::MonitorAutoLock lock(job->mon);
done = job->done;
- if (!done) job->abandoned = true; job->req.cancelled = true;
+ if (!done) { job->abandoned = true; job->req.cancelled = true; }
}
if (done) delete job;
}
@@ -1328,6 +1408,33 @@ jihad_offscreen_compose(nsIWidget* aWidget, nsIDocShell* aDocShell, void* aDest,
aWidth, aHeight);
}
+// T-515: the frame's halves on their own (see PuppetWidget::JihadPaintLayers, JihadComposePost).
+// paint_layers: the main thread; compose_post: any thread. aUseView 0: the view last set.
+MOZ_EXPORT int
+jihad_offscreen_paint_layers(nsIWidget* aWidget, nsIDocShell* aDocShell)
+{
+ return (aWidget && static_cast<PuppetWidget*>(aWidget)->JihadPaintLayers(aDocShell)) ? 1 : 0;
+}
+MOZ_EXPORT int
+jihad_offscreen_compose_peek(nsIWidget* aWidget, uint32_t aToken)
+{
+ return aWidget ? static_cast<PuppetWidget*>(aWidget)->JihadComposePeek(aToken) : -1;
+}
+MOZ_EXPORT uint32_t
+jihad_offscreen_compose_post(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)
+{
+ if (!aWidget) {
+ return 0;
+ }
+ const float view[5] = { (float)aScale, (float)aOffX, (float)aOffY, (float)aScrollX, (float)aScrollY };
+ return static_cast<PuppetWidget*>(aWidget)->JihadComposePost(aDest, aStride, aWidth, aHeight, aRowLo, aRowHi,
+ aRow2Lo, aRow2Hi, aShift, aSrc, aSrcY, aFlags,
+ aUseView ? view : nullptr, aScrollValid != 0);
+}
+
// T-502: the frame in two halves (see PuppetWidget::JihadComposeStart).
MOZ_EXPORT uint32_t
jihad_offscreen_compose_start(nsIWidget* aWidget, nsIDocShell* aDocShell, void* aDest, int aStride,
diff --git a/widget/PuppetWidget.h b/widget/PuppetWidget.h
index e9a04e0117..b68c975777 100644
--- a/widget/PuppetWidget.h
+++ b/widget/PuppetWidget.h
@@ -13,6 +13,7 @@
#ifndef mozilla_widget_PuppetWidget_h__
#define mozilla_widget_PuppetWidget_h__
+#include "mozilla/Mutex.h" // Jihad T-515
#include "mozilla/gfx/2D.h"
#include "mozilla/RefPtr.h"
#include "nsBaseScreen.h"
@@ -294,7 +295,18 @@ public:
// 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; aScrollOnly: the composite's only change was the card's scroll
int JihadComposePoll(uint32_t aToken, bool aWait, int* aOut, bool* aScrollOnly = nullptr);
+ // T-515: the frame's two halves on their own. PaintLayers (the main thread) flushes and paints the
+ // layers, as Start does first; Post (any thread: the express lane while the main thread runs the
+ // page's script) composes the layer tree as it stands and reads it, at the view given (aView:
+ // scale, offset x, offset y, scroll x, scroll y; null = the view last set) and aScrollValid. The
+ // token is polled and forgotten as any other.
+ bool JihadPaintLayers(nsIDocShell* aDocShell);
+ 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);
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);
void JihadComposeAbandonAll();
void JihadSetScroll(double aX, double aY, bool aValid) { mJihadScrollX = (float)aX; mJihadScrollY = (float)aY; mJihadScrollValid = aValid; }
mozilla::layers::CompositorBridgeParent* JihadBridge();
@@ -449,6 +461,10 @@ private:
bool mJihadSharedLayerManager = false; // T-502: mLayerManager is the parent's (a child widget)
float mJihadScale = 1.0f; // T-502: the card's zoom, applied by the compositor
std::vector<::JihadReadJob*> mJihadJobs; // T-502: frames in flight on the compositor thread
+ mozilla::Mutex mJihadJobsLock{"jihad-compose-jobs"}; // T-515: the list from the main thread and the express lane
+ 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);
uint32_t mJihadNextToken = 1;
float mJihadScrollX = 0.0f, mJihadScrollY = 0.0f; // T-502: the card's scroll for the compositor
bool mJihadScrollValid = false;
@@ -0,0 +1,32 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: NotAlexNoyle <alexnoyle@icloud.com>
Date: Mon, 19 Oct 2026 03:52:26 -0400
Subject: [PATCH] layers: a composite acknowledged after the widget is gone
tells nobody
ClientLayerManager::DidComposite dereferenced mWidget, which the widget's teardown nulls. The
embedder's offscreen compose jobs can be acknowledged after that: a Jihad card closed while a job it
had asked for was waited out, the job composed, and its DidComposite reached the main thread after
the page was gone (SIGSEGV at address 0 in a device round, 2026-10-19). Nothing is left to tell then.
---
gfx/layers/client/ClientLayerManager.cpp | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/gfx/layers/client/ClientLayerManager.cpp b/gfx/layers/client/ClientLayerManager.cpp
index 95af8fb5fc..2f34442d1c 100644
--- a/gfx/layers/client/ClientLayerManager.cpp
+++ b/gfx/layers/client/ClientLayerManager.cpp
@@ -441,7 +441,12 @@ ClientLayerManager::DidComposite(uint64_t aTransactionId,
const TimeStamp& aCompositeStart,
const TimeStamp& aCompositeEnd)
{
- MOZ_ASSERT(mWidget);
+ // Jihad (T-515): the embedder's offscreen compose jobs can be acknowledged after the widget is
+ // gone (a card closed while a job it had asked for was waited out): nothing is left to tell. The
+ // widget's teardown nulls mWidget; it was dereferenced here (SIGSEGV at 0, 2026-10-19).
+ if (!mWidget) {
+ return;
+ }
// |aTransactionId| will be > 0 if the compositor is acknowledging a shadow
// layers transaction.
+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=53e50a5fe04411c02230e4380be04fd18a60af07
UXP_PIN=2cc21a9a5e6ded469477d5805c52489ba6a4ecb6
# 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 = "53e50a5fe04411c02230e4380be04fd18a60af07"
SRCREV = "2cc21a9a5e6ded469477d5805c52489ba6a4ecb6"
S = "${WORKDIR}/git"
# Modern C++14 toolchain (device-build R1) — the stock dylan/device gcc cannot build UXP.
+4 -2
View File
@@ -1,6 +1,6 @@
---
created: "2026-10-18"
last_edited: "2026-10-18"
last_edited: "2026-10-19"
---
# Implementation Tracking: gpu-rendering
@@ -12,7 +12,9 @@ Build site: context/plans/build-site.md
| T-503 | DONE | Rounds 138-150: fixed/sticky at minimum cost, verify trustworthy, the root composition. Commit `e74e212d`. |
| T-504 | PARTIAL | Rounds 162-186. `render/goanna/BrowserPageGoanna.cpp` (the zoom hold, gpuZoomRelease, the settle that keeps the compositor's scroll where the engine cannot reach, `JIHAD_GPU_ZOOM_DUMP`), `GoannaRenderPage.cpp` (the resolution above 1.1 in SetGpuViewport), engine 0044 (`gfx/gl/GLTextureImage.cpp`: 1x1 conversion, pool) and 0045 (`CompositorOGL.cpp` DEAA rule; slow composite/draw logs), `render/goanna/test/enyo-selftest.js` (`t` double tap), `tools/device/gpu-bench.c` sections 11-16, `tools/device/t502/zoom-run.sh`. Open: the first frame while the page's script holds the main thread (with T-505). |
| T-505 | PARTIAL | Rounds 187-201. Overscan: `BrowserPageGoanna.cpp` (the GPU branch's frame `gpuLo`/`gpuFrameH`, the view row in the world transform via `gpuSetView`, rows outside the page filled white, verify on the page rows), `GoannaRenderPage.cpp` (`SetGpuFrameHeight`, `SetGpuOverscan`, frame-height reads), `EngineCompat.h`, engine 0046 (`nsLayoutUtils`, `nsGfxScrollFrame`). Frames capped at the compositor's first pbuffer (1400 rows: a 1412-row frame read its top rows from the bottom). `tools/device/t502/pan-run.sh` sizes its dumps by their bytes. Table and gap trace in the build site. Open: Wikipedia ≥ 24 fps (T-515). |
| T-515 | PENDING | Frames off the main thread (IPC thread, compositor-published frames). |
| T-515 | PARTIAL | Rounds 240-267. `render/browserserver/Yap/YapProxy.{h,cpp}` (a reader thread per proxy, the queue and wake pipe, the express and dispatch hooks, the send lock, commands tagged in arrival order, `injectAsync`), `render/goanna/JihadFrameLock.h` (the frame lock, the want lock, the main thread's away mark), `render/browserserver/JihadBrowserServer.{h,cpp}` (the hooks, the tick's lock let go of around the pump, the inject pan and zoom through the card's path, the express thread stopped at exit), `render/goanna/BrowserPageGoanna.{h,cpp}` (the express lane: `expressWant`/`expressReturn`/`expressStep`, `xStart`/`xPoll`/`xSnapshot`/`xLeads`; the main lane's paint window, dropped frames older than an express publish, the zoom hold released by an express frame, `JIHAD_GPU_TIMELINE`, `JIHAD_GPU_ZOOM_DUMP=log`; a 32-bit `1L << 40` fixed), `GoannaRenderPage.{h,cpp}` (`PaintLayers`, `ComposePost`, `ComposePeek`), `EngineCompat.h`, engine 0048 and 0049 (a composite acknowledged after the widget is gone); `tools/device/t502/zoom-seq.sh`, `render/goanna/test/jihad-script-hold.html`. Met: zooms under script within 250 ms; open: T-516, T-517. |
| T-516 | PENDING | The zoom out's slow draws of the other resolution's tiles (R6). |
| T-517 | PENDING | Desktop Wikipedia's ~45 ms compositor frame (R3). |
| T-506 | DONE | Rounds 202-203. `render/browserserver/JihadBrowserServer.cpp` (click, key, text and jsurl injections mark the time), `BrowserPageGoanna.cpp` (`MarkInputLatency`; the GPU publish logs "input latency" for the first frame with changed rows). Measured: anim 26-31 fps, typing 22-62 ms, DOM edits 26-70 ms, self-scroll 10.7-13.9 fps. |
| T-507 | DONE | Rounds 204-212. `BrowserPageGoanna.cpp` (the thumbnail from the published frame's view row `mGpuJobViewRow`; `CompositePopups` at the published job's position and zoom), `render/browserserver/JihadBrowserServer.cpp` (`thumb <path>` inject), `render/goanna/test/enyo-selftest.js` (`<` back, `>` forward, `R` reload, `O<orientation>`), `render/goanna/test/jihad-card-events.html`. |
| T-508 | DONE | Rounds 213-228. `build/webos-oe/push-engine-update.sh` (FFmpeg's libraries), `render/goanna/test/jihad-video-styled.html`, `jihad-anim-opacity.html`, `jihad-canplay.html`. Measured on the device: four codecs to the end, styled video, Flash anim and keys. |
+9
View File
@@ -211,3 +211,12 @@ Build site: context/plans/build-site.md
- **Files:** render/goanna/BrowserPageGoanna.{cpp,h}, render/goanna/GoannaRenderPage.{cpp,h}, engine patch 0047, tools/device/t502/three-daemons*.sh
- **Validation:** Build P; device rounds 229-232; Acceptance 4/4 (R1's two, R10's two)
- **Next:** T-510 — the GPU path as the default (blocked by T-504's open criterion; T-515 first)
### Iteration 8 — 2026-10-19
- **Task:** T-515 — frames made off the main thread (the express lane)
- **Tier:** 87
- **Status:** PARTIAL
- **Files:** render/browserserver/Yap/YapProxy.{h,cpp}, render/browserserver/JihadBrowserServer.{h,cpp}, render/goanna/JihadFrameLock.h, render/goanna/BrowserPageGoanna.{cpp,h}, render/goanna/GoannaRenderPage.{cpp,h}, render/goanna/EngineCompat.h, engine patch 0048, tools/device/t502/zoom-seq.sh, render/goanna/test/jihad-script-hold.html
- **Validation:** Build P; device rounds 240-267; zooms under script 10/10 within 250 ms (Wikipedia after load, the script-hold page); script-hold pans 26-32 fps; no regression with it off (GPU 30-33, software 2.9-3.2); clean exit. Wikipedia pans 19-21 fps (open: T-517); double taps' slow draws (open: T-516)
- **Next:** T-516 or T-517 (T-510 waits on T-504, which waits on T-516)
+3 -3
View File
@@ -56,9 +56,9 @@ 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
- [ ] 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 *(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).)*
- [ ] 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
- [ ] Frame gaps of 56 ms or more during a pan are accounted for (traced to their source) and the avoidable ones removed *(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.)*
- [ ] Frame gaps of 56 ms or more during a pan are accounted for (traced to their source) and the avoidable ones removed *(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)*
**Dependencies:** R2
@@ -82,7 +82,7 @@ Hacker News 36-37, desktop Wikipedia 13-14 (software 9-12.4).
**Description:** A pinch or a double-tap zoom shows the page at the new zoom promptly and never leaves part of the card grey.
**Acceptance Criteria:**
- [x] A scripted pinch in to about 1.7 and back out to the fit zoom (and a double-tap zoom) on desktop en.wikipedia.org and on `jihad-long-static.html` is reproduced on the bench with the frame timeline logged *(2026-10-18: the card's own gesture calls through its test hook, `g` pinches and `t` double taps; Wikipedia 0.69 → 1.23 → 2.20 → 1.51 → 1.03, double tap 1.26 and back to 0.69; the test page 0.79 → 1.41 → 2.51 → 1.72 → 1.18 and its double taps; each zoom's "the first frame at the new zoom is out" line)*
- [ ] The first frame at the new zoom reaches the card within 250 ms of the zoom command, and the frames after it keep the pan rate of R3 *(2026-10-18, PARTIAL: on a settled page 52-217 ms for pinches, 61-124 ms for a double tap in and 182-257 ms back out, one double tap in at 384 and 414 ms in two runs of ten; a pan at 1.23 runs 18.5-28 fps, the fit zoom's rate. While the page's own script holds the main thread (desktop Wikipedia's modules for ~10 s after its load) the daemon cannot even read the zoom command: 0.8-1.7 s. That part waits on frames made off the main thread, with T-505.)*
- [ ] The first frame at the new zoom reaches the card within 250 ms of the zoom command, and the frames after it keep the pan rate of R3 *(2026-10-18, PARTIAL: on a settled page 52-217 ms for pinches, 61-124 ms for a double tap in and 182-257 ms back out, one double tap in at 384 and 414 ms in two runs of ten; a pan at 1.23 runs 18.5-28 fps, the fit zoom's rate. While the page's own script holds the main thread (desktop Wikipedia's modules for ~10 s after its load) the daemon cannot even read the zoom command: 0.8-1.7 s. That part waits on frames made off the main thread, with T-505. 2026-10-19, T-515: with the frames made off the main thread the zoom command is read on a thread of its own and the frame composed while the script runs: ten zooms each on Wikipedia after its load 10/10 within 160-225 ms (median 58-77; held off: median 681, max 1116, three never framed), on a page holding the main thread 600 ms in 700 within 45-91 ms (held off: median 450, max 697). Card double taps 40-449 ms: the slow ones are GPU draws of the other resolution's tiles after a zoom in's repaint (20-35 ms a draw on the Adreno), on either lane: T-516.)*
- [x] No area of the card is left grey or blank at rest: within 500 ms of the gesture's end every row of the card has content at the new zoom (dumps looked at) *(2026-10-18: `JIHAD_GPU_ZOOM_DUMP` keeps the frame the card shows at rest; the last frame of each zoom came 344-612 ms after it and covers the card: Wikipedia at 1.23, 2.20, 1.51, a double tap at 1.26, the test page at 2.51 and 1.72. Zoomed in, the settle had published frames at the engine's x under a card at another x, a grey strip down the right; fixed)*
- [x] Tile uploads do not each wait behind queued GPU work (the Adreno 220 serialises an upload with pending GPU work: 12-63 ms per 256x256 tile seen) *(2026-10-18: the wait was the driver converting a texture on its first write after its first draw, ~18 ms per 256x256, once per texture (`gpu-bench.c` sections 11-16); engine patch 0044 converts each new tile texture at 1x1 (1.1-2 ms) and pools textures: re-uploads slower than 8 ms from 55-59 a Wikipedia zoom run to one)*
- [x] Zoomed in, the page is painted at the zoom (text as sharp as the software path's), not a 1x paint magnified *(added 2026-10-18 from the owner's "zoom rendering of page content lags behind actual page zoom state": above a zoom of 1.1 the pres shell's resolution follows the zoom; Wikipedia at 2.2 compared with the 1x paint, crisp and aligned to the pixel)*
+5 -3
View File
@@ -505,8 +505,8 @@ 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 | 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) |
| 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 | 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)) |
| 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 | 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) |
| 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 | 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) |
| 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`) |
@@ -516,7 +516,9 @@ the mid-pan dumps, never by a screenshot. No device run while the owner is at th
| T-512 | Fit to keep: diagnostics pruned and bounded, the knobs listed, the engine work folded into the patch queue, the docs and README | cavekit-gpu-rendering.md | R11 | T-503…T-511 | no | M | pending |
| T-513 | Review of Tier 87 (Codex by direct invocation, the inspector as backup) and fixes | cavekit-gpu-rendering.md | R11 | T-512 | no | M | pending |
| T-514 | [OWNER] Finger round on the GPU path in all three interfaces: pan, flick, pinch, double-tap | cavekit-gpu-rendering.md | R12 | T-513 | yes | S | BLOCKED-EXTERNAL (a person at the device) |
| T-515 | Frames made off the main thread: the card's scroll and zoom read on an IPC thread, the compositor composing and the frame published while the main thread runs the page's script or a paint (the open criteria of T-504, the first frame at a new zoom within 250 ms under script, and T-505, desktop Wikipedia ≥ 24 fps) | cavekit-gpu-rendering.md | R3, R6 | T-505 | yes | L | pending |
| T-515 | Frames made off the main thread: the card's scroll and zoom read on an IPC thread, the compositor composing and the frame published while the main thread runs the page's script or a paint (the open criteria of T-504, the first frame at a new zoom within 250 ms under script, and T-505, desktop Wikipedia ≥ 24 fps) | cavekit-gpu-rendering.md | R3, R6 | T-505 | yes | L | PARTIAL 2026-10-19 (rounds 240-267, the express lane: each card's YAP commands read on a thread of the proxy's own, the daemon's frame lock (`render/goanna/JihadFrameLock.h`; the main thread lets go of it in the pump and the layer paint), an express thread that composes the layer tree as it stands and publishes, engine 0048 (the paint and the compose post apart, jobs from any thread). Met: the zoom under script (Wikipedia after load 10/10 within 160-225 ms, held off median 681 and three never framed; a page holding the main thread 600 ms in 700 within 45-91 ms, held off median 450); pans on that page 26-32 fps (2.4-3.8); Wikipedia's 2.2 s restyle under frames every ~45 ms. Open, and no longer the main thread's: desktop Wikipedia 19-21 fps, the compositor thread's ~45 ms a frame (T-517); double taps 40-449 ms, GPU draws of the other resolution's tiles (T-516). `docs/rounds/t502-2026-10-17/express-r240-267.txt`) |
| T-516 | The zoom's first frame on every gesture: a zoom out after a zoom in's repaint draws the new resolution's tiles at ~0.55, 20-35 ms a draw on the Adreno (a 179 ms composite, round 263); found and removed, card double taps both ways within 250 ms in runs of ten | cavekit-gpu-rendering.md | R6 | T-515 | yes | M | pending |
| T-517 | Desktop Wikipedia's pan frame under 41 ms: the compositor thread's ~45 ms a frame (3-7 ms of CPU a composite, the column passes, the readback's wait for the GPU) broken down and cut, whichever lane asks; ≥ 24 fps in the scripted pan | cavekit-gpu-rendering.md | R3 | T-515 | yes | L | pending |
## Tier 86 — Content blocking: ads and trackers (2026-10-15; added to the site, not a regeneration)
+3 -1
View File
@@ -13,7 +13,7 @@ available is met.
`RB_20261006` (`5f290d1097a8350b56ce2092aa9d5fc9147c6661`, 2026-10-04) — `UXP_TAG` / `UXP_BASE` in
`build/engine/engine-version.env`. (Until 2026-10-12: master `b2594a4`, 2026-06-30.)
- **Our modified engine:** the branch `jihad/uxp-RB_20261006` of the fork `NotAlexNoyle/UXP-Jihad`,
at `UXP_PIN` (`53e50a5fe04411c02230e4380be04fd18a60af07`), present in this repository as the
at `UXP_PIN` (`2cc21a9a5e6ded469477d5805c52489ba6a4ecb6`), present in this repository as the
submodule `third_party/uxp`. The device build compiles that tree.
- **The modifications as patches:** `build/desktop/patches/` holds every change as a patch
against `UXP_BASE`. `build/engine/patch-queue.sh --check` proves the patches, applied in
@@ -73,6 +73,8 @@ modifications; the same changes are the commits of `jihad/uxp-RB_20261006`, one
| `0045-gpu-compositor-no-edge-antialiasing-for-an-axis-alig.patch` | `gfx/layers/opengl/CompositorOGL.cpp`, `gfx/layers/ipc/CompositorBridgeParent.cpp` | T-504: on the GPU path an axis-aligned scale gets no edge antialiasing (its shader variants compiled on first use, ~150 ms each, in the first frame of a zoom); `JIHAD_GPU_PHASES` also logs each slow composite's phases and each slow draw |
| `0046-gpu-compositor-the-root-scroll-frame-s-clip-widened-.patch` | `layout/base/nsLayoutUtils.{h,cpp}`, `layout/generic/nsGfxScrollFrame.cpp` | T-505: `jihad_layout_set_overscan` widens the root scroll frame's clip on the GPU path by the rows the daemon's frame carries above and below what the card shows (overscan), so the card has content to pan into between frames |
| `0047-gpu-compositor-a-given-up-frame-writes-nothing-test-.patch` | `gfx/layers/ipc/CompositorBridgeParent.cpp`, `gfx/layers/opengl/CompositorOGL.{h,cpp}`, `widget/PuppetWidget.cpp` | T-509: a compose job the daemon forgot writes nothing into its slot (`cancelled`); test switches `JIHAD_GPU_TEST_NO_EGL` (no GL context, as an EGL failure) and `JIHAD_GPU_TEST_STALL` (a job held 6 s) |
| `0048-gpu-compositor-the-frame-s-paint-and-its-compose-pos.patch` | `widget/PuppetWidget.{h,cpp}` | T-515: the frame's layer paint (`jihad_offscreen_paint_layers`, the main thread) and its compose job (`jihad_offscreen_compose_post`, any thread, at a view and scroll it is given) apart, so the daemon's express lane composes and publishes frames while the main thread runs the page's script; the job list under a mutex (a poll without a wait holds it throughout); `jihad_offscreen_compose_peek` (a job's state without its result); a forgotten job's cancelled flag only with abandoned |
| `0049-layers-a-composite-acknowledged-after-the-widget-is-.patch` | `gfx/layers/client/ClientLayerManager.cpp` | T-515: `DidComposite` returns when the widget is gone (a card closed while a compose job it had asked for was waited out; its acknowledgement reached the main thread after the page was torn down: SIGSEGV at 0) |
### Branding strip (cavekit-licensing-branding R3) — patch 0010
+6
View File
@@ -26,6 +26,12 @@ Run each item in all three variants (Enyo, Mochi, Mojo) unless it says otherwise
header stays put, the text is continuous (no repeated or missing bands), nothing blank enters
from below, no jump every ~660 px (the engine's anchor moves). Then flick. A scripted pan cannot
show this: the adapter's own view does not move. (T-502; offscreen-rendering, the scroll row)
- [ ] The GPU path with the express lane (T-515, 2026-10-19; on by default once T-510 lands): right
after desktop en.wikipedia.org loads (its scripts run for seconds), pinch in, pan, pinch out and
double-tap at once, and do the same on `jihad-script-hold.html` (the host server; its script holds
the engine 600 ms in every 700): the page follows the finger at the new zoom without waiting for
the script, nothing flips back to the old zoom, and at rest the text is sharp and nothing is grey.
(cavekit-gpu-rendering R3, R6, R12)
## Keyboard and text
- [ ] Tap a field on several pages in one session: the keyboard comes up every time, and the page
+39
View File
@@ -52,6 +52,45 @@ Jihad Browser, and it starts when Jihad Browser is finished.
Memory: `global-jihad-project.md`. The workspace `CLAUDE.md` carries the pointer and the corrected
target line (both webOS 3.0.5 and CE 3.1.0).
## 2026-10-19 delta — T-515: frames made off the main thread, the express lane (READ FIRST)
**What it is.** With `JIHAD_GPU_EXPRESS=1` (and the GPU path) each card's YAP commands are read on a
thread of the proxy's own (`render/browserserver/Yap/YapProxy.cpp`), and the daemon's frame
bookkeeping is under one frame lock (`render/goanna/JihadFrameLock.h` explains it) that the main
thread lets go of only in the engine's pump and in a frame's layer paint. An express thread
(`BrowserPageGoanna.cpp`, `expressStep`) composes the layer tree as it stands at the card's newest
scroll and zoom and publishes the frame itself: while the card moves it makes every frame (the main
lane paints layers and moves the anchor), and otherwise it fills the main thread's stalls. Engine
patch 0048 splits the frame's paint from its compose post and lets any thread post and poll jobs.
It is OFF by default, as the GPU path still is: T-510 turns both on.
**What it did (rounds 240-267, `docs/rounds/t502-2026-10-17/express-r240-267.txt`).** A zoom while the
page's script holds the main thread: Wikipedia after its load 10/10 within 160-225 ms (without the
lane: median 681 ms, max 1116, three zooms never framed); a page holding the main thread 600 ms in
every 700 (`render/goanna/test/jihad-script-hold.html`) within 45-91 ms (without: median 450). That
page pans at 26-32 fps (2.4-3.8). Desktop Wikipedia pans at 19-21 fps: the page's 2.2 s restyle now
runs under frames every ~45 ms, and the floor is the compositor thread's ~45 ms a Wikipedia frame
whichever lane asks (T-517). Card double taps 40-449 ms: the slow ones are GPU draws of the other
resolution's tiles after a zoom in (T-516). No regression with it off; clean daemon exit.
**Traps.** (1) Two locks: the frame lock (the main thread, the express thread, the inject threads)
and the want lock (the reader thread's scroll and zoom; never held while waiting). The reader
thread never waits for the frame lock: a proxy's teardown joins it (a returned buffer tries for 4 ms
and is left to the main loop). (2) With the lane on, the inject channel's `pan` and `zoom` go
through the card's own path on a thread (`zoom <z> <x> <y> [delay ms]`), so they arrive while the
main thread is busy; without it they are applied in the tick as before. The inject file itself is
read only when the main thread is free. (3) "Served" is by command, not by position: commands are
tagged in arrival order by the proxy and the main lane serves the card once its frame took up the
newest (the engine clamps and rounds positions; comparing them made the lane redraw every frame).
(4) A main-lane frame posted before an express publish is dropped at its poll; one at a zoom the
main loop has not taken up yet is held (300 ms at most). (5) `JIHAD_GPU_ZOOM_DUMP=<path>` frames are
3-4 MB each on a 40 MB `/tmp`: use `JIHAD_GPU_ZOOM_DUMP=log` for timelines, delete files at once.
(6) A scripted pan's screenshot is meaningless (the card's own scroll does not move); judge by the
dumps, `JIHAD_GPU_TIMELINE=1` and the paint pipeline lines.
**Next.** T-516 (the zoom out's slow draws: R6, unblocks T-504 and so T-510) and T-517 (Wikipedia's
compositor frame: R3). Knobs in `tools/device/t502/README.md` (T-515 paragraph).
## 2026-10-18 (night) delta — T-385…T-387 done, T-504 (zoom) mostly done (READ FIRST)
**Engine-update readiness (T-385…T-387) done** and reviewed (the inspector, Tier 74: APPROVE, the
@@ -0,0 +1,51 @@
T-515, the express lane (frames made off the main thread): rounds 240-267, 2026-10-19, CE 3.1.0 TouchPad.
Daemon env JIHAD_GPU_COMPOSITOR=1 (gpu-spike-start.sh) plus JIHAD_GPU_EXPRESS=1; baselines with the
express lane held off (JIHAD_GPU_EXPRESS_MS=999999 JIHAD_GPU_EXPRESS_LEAD_MS=0: the reader thread and
the inject path as with it on, no express frames).
Pans (pan-run.sh, inject pan 0 -> 3000 px in 10 s through the card's path; paint pipeline 2 s windows)
jihad-script-hold.html (600 ms of script in every 700)
express off (r241-hold-main) 2.4 2.9 3.8 3.3 fps
express (r267-hold-x) 27.1 26.2 31.9 fps (the last window after the pan's end)
desktop en.wikipedia.org/wiki/HP_TouchPad (after its load)
express off, T-505's table 16.5-24 fps (overscan 0.15)
express, fills stalls only (r246) 16.4 16.8 19.0 fps
express leads while the card moves (r249) 19.5 19.0 21.0 fps
the same, timeline (r259/r260) median gap 45-46 ms, mean 50-56, p90 71-75
overscan 0.08 / 0 (r251) median 42 / 42 ms: frame area is not the cost
band surface 600 / 942 rows (r265) median 45 / 46 ms: column chunk count is not the cost
fixed rows read as a band (r253) median 69 ms (the columns stay)
second frame posted early (r266) median 43 ms, each frame 100-176 ms old: reverted (JIHAD_GPU_EXPRESS_EARLY)
The main thread's stalls no longer gap the frames: Wikipedia's 2.2 s restyle on the first scroll ran
under express frames every ~45 ms (r249 timeline). What is left is the compositor thread's ~45 ms a
Wikipedia frame, whichever lane asks for it (phases: render layers 3-7 ms of CPU a composite; the rest
the readback's wait for the GPU and the column passes).
jihad-long-static.html, express off (r264, regression check) 30.0-32.9 fps (T-505's table: 30-35)
jihad-long-static.html, NOGPU (r264) 2.9-3.2 fps (2.7-3.2)
Zooms from their arrival (zoom-seq.sh, 10 zooms, sent 0/150/300/450/600 ms into the script's hold)
jihad-script-hold.html express off (r245-base) median 450 ms, max 697
express (r245, r267) median 87 / 41 ms, max 91 / 45
Wikipedia after load express off (r254-base) median 681 ms, max 1116, 3 zooms never framed
express (r255, r260, r267) 10/10 each: max 160 / 195 / 225 ms, median 69 / 77 / 58
Card double taps on Wikipedia (card-selftest t300,400 x10-12; first frame at each zoom, either lane):
r261 in 448, 65, 449, 245; out 84, 83, 84, 145 ms
r263 in 69, 71, 182, 68, 150, 177; out 46, 40, 312, 236, 314, 197, 160, 157 ms
The slow ones are GPU draws: a zoom out after a zoom in's repaint draws the new resolution's tiles at
0.545, 20-35 ms a draw (slow draw lines, a 179 ms composite in r263); the main lane's are the same.
A card pinch (r257): the frame at 1.23 59 ms after the card's command; the card at rest sharp and covered.
Express zoom frame vs the main lane's at rest (r256 dumps zd.x3 / zd.4 at 1.69): the same view rows.
Bugs found and fixed on the way: wants waited on main jobs composed but unpolled (r241: the main thread
sat in the script; peek added, engine 0048); a main frame at the old zoom ended the card's wait (r254);
a frame position the engine clamps never "served" the card, so the express lane redrew it after every
main frame (r258 duplicates) - the card's commands are tagged in arrival order and the main lane serves
the card once its frame took up the newest; commands that arrive before the page exists (r259) are
why the tag is the proxy's, not a count; a stale load-time want framed before the main lane's first
frame (r246) - the main lane's first frame of a geometry or document comes first.
Rounds 268-269: a SIGSEGV at 0 after a card disconnected and reconnected (r268, the express lane on):
ClientLayerManager::DidComposite reached the main thread after the page was torn down (a main-lane job
composed while the page's jobs were waited out) and dereferenced the destroyed widget. Engine 0049
returns when the widget is gone; r269 repeated the gesture and disconnect run with the lane on and off,
no fault.
+129 -6
View File
@@ -4,12 +4,16 @@
/* Jihad Browser — JihadBrowserServer implementation. See header. */
namespace jihad { void GpuDumpRestart(); } // GoannaRenderPage.cpp (T-502 round 133)
namespace jihad { void MarkInputLatency(); } // BrowserPageGoanna.cpp (T-506: input to the first changed frame)
namespace jihad { void jihadTimelineMain(const char* what, long ms); } // BrowserPageGoanna.cpp (T-515)
#include "JihadBrowserServer.h"
#include "../goanna/JihadRuntimePaths.h" // the ONE runtime-state dir (T-057 / R8)
// A card connected or left → the engine side tells the components (render/goanna/EngineHost.cpp;
// declared here as a plain prototype: EngineHost.h carries XPCOM headers this side is not built with).
namespace jihad { void JihadNotifyClients(bool connected, int clients); }
#include "JihadLunaService.h" // LunaServiceReadWebosProxy (a card opened)
#include "../goanna/JihadFrameLock.h" // T-515: the express lane's frame lock
#include <YapDefs.h> // kMaxMsgLen (the inject pan's packets)
#include <thread>
#include <algorithm>
#include <string>
#include <cstdio>
@@ -71,9 +75,51 @@ void ProxySink::msgAddFlashRects(const char* j) { mSrv->msgAddFlashRects(mPro
void ProxySink::msgRemoveFlashRects(const char* j) { mSrv->msgRemoveFlashRects(mProxy, j); }
// ---- server ----------------------------------------------------------------
// T-515, the express lane (render/goanna/JihadFrameLock.h): each card's commands are read on a
// thread of the YAP proxy's own. The card's scroll, zoom and returned buffers go to the page's
// express lane there, at once (the frame lock taken); every command still reaches the main loop
// as before, where the dispatch hook holds the frame lock around it.
static JihadBrowserServer* sExpressServer = nullptr;
// each proxy's page for the reader threads, under the want lock (JihadFrameLock.h): set at the
// page's connect, dropped before its delete (reap)
static std::map<YapProxy*, jihad::BrowserPageGoanna*>* sExpressPages = nullptr;
static bool jihadExpressHook(YapProxy* proxy, const uint8_t* data, int len, uint32_t tag) {
if (!sExpressServer || !data || len < 3) return false;
YapPacket* pkt = YapProxy::wrapPacket(const_cast<uint8_t*>(data), len);
int16_t cmd = 0;
(*pkt) >> cmd;
if (cmd == 0x1500 || cmd == 0x150e) { // SetScrollPosition, SetZoomAndScroll: the want lock only
double zoom = 0.0; int32_t cx = 0, cy = 0;
if (cmd == 0x150e) (*pkt) >> zoom;
(*pkt) >> cx; (*pkt) >> cy;
std::lock_guard<std::mutex> l(jihad::jihadWantLock());
auto it = sExpressPages ? sExpressPages->find(proxy) : std::map<YapProxy*, jihad::BrowserPageGoanna*>::iterator();
if (sExpressPages && it != sExpressPages->end()) it->second->expressWant(cx, cy, zoom, cmd == 0x150e, tag);
} else if (cmd == 0x150d) { // ReturnBuffer: the frame lock, if it comes within 4 ms
int32_t key = 0;
(*pkt) >> key;
if (jihad::jihadFrameLock().tryLockFor(4)) {
if (jihad::BrowserPageGoanna* p = sExpressServer->pageQuiet(proxy)) p->expressReturn(key);
jihad::jihadFrameLock().unlock();
}
}
YapProxy::freePacket(pkt);
return false; // the main loop handles it too (returnBuffer skips the clear made here)
}
static void jihadDispatchHook(bool begin) {
if (begin) jihad::jihadFrameLock().lock();
else jihad::jihadFrameLock().unlock();
}
JihadBrowserServer::JihadBrowserServer(const char* name, jihad::EngineHost& host)
: BrowserServerBase(name), mHost(host), mInTick(false) {
jihad::SetDownloadSink(this); // receive engine download/handoff callbacks
if (jihad::jihadExpressOn()) {
sExpressServer = this;
YapProxy::s_expressHook = &jihadExpressHook;
YapProxy::s_dispatchHook = &jihadDispatchHook;
printf("[jihad-bs] express lane: each card's commands read on a thread of its own (T-515)\n");
}
#ifndef JIHAD_NO_INJECT
// Resolve the self-drive channel ONCE (see the block above processInjectFile
// for what it is and how to turn it on). Disabled resolves to "", which makes
@@ -85,6 +131,10 @@ JihadBrowserServer::JihadBrowserServer(const char* name, jihad::EngineHost& host
}
JihadBrowserServer::~JihadBrowserServer() {
if (jihad::jihadExpressOn()) jihad::jihadExpressStop(); // T-515: before the lock (it waits for the thread's step)
jihad::FrameLockGuard frameLock; // T-515
if (sExpressServer == this) sExpressServer = nullptr;
if (sExpressPages) { std::lock_guard<std::mutex> l(jihad::jihadWantLock()); sExpressPages->clear(); }
jihad::SetDownloadSink(nullptr); // clear before we (the sink) are destroyed
for (auto& kv : mPages) { delete kv.second.page; delete kv.second.sink; }
for (auto& e : mReap) { delete e.page; delete e.sink; }
@@ -194,6 +244,11 @@ void JihadBrowserServer::reap(YapProxy* proxy) {
if (it == mPages.end()) return;
Page pg = it->second;
mPages.erase(it);
if (sExpressPages) { // T-515: out of the reader thread's reach before the delete
std::lock_guard<std::mutex> l(jihad::jihadWantLock());
sExpressPages->erase(proxy);
}
if (pg.page) pg.page->expressDetach(); // T-515: and the express thread's: the proxy goes now
// F-1: RE-ELECT rather than null. mLastProxy is both the self-drive inject
// target and the download fallback; clearing it when the newest card closed
// meant every later download message — terminal included — was discarded even
@@ -307,7 +362,45 @@ void JihadBrowserServer::processInjectFile() {
// T-502 device rounds: a scripted finger pan, y from x to y over n ms, a setScrollPosition
// per tick (what the adapter sends for a drag)
printf("[jihad-bs] inject pan %d -> %d over %d ms\n", x, y, n);
mPanPage = p; mPanY0 = x; mPanY1 = y; mPanMs = n > 0 ? n : 1; mPanT0 = jihadTickNowMs(); mPanActive = true;
YapProxy* panProxy = nullptr;
for (auto& kv : mPages) if (kv.second.page == p) panProxy = kv.first;
// a setScrollPosition for the adapter's drag (a write packet's buffer is kMaxMsgLen long)
auto scrollPacket = [](std::vector<uint8_t>& buf, int py) {
buf.assign(kMaxMsgLen, 0);
YapPacket* w = YapProxy::wrapPacket(buf.data(), 0);
(*w) << (int16_t)0x1500; (*w) << (int32_t)0; (*w) << (int32_t)py; (*w) << (int32_t)0; (*w) << (int32_t)0;
const int len = w->length();
YapProxy::freePacket(w);
return len;
};
std::vector<uint8_t> probe;
{ const int plen = scrollPacket(probe, x);
if (!(jihad::jihadExpressOn() && panProxy && panProxy->injectAsync(probe.data(), plen))) panProxy = nullptr; }
if (panProxy) {
// T-515: the card's own path. A thread sends the setScrollPosition the adapter sends for a
// drag, every 16 ms, through the proxy's reader side (the express hook, then the main
// loop's queue), so the pan goes on while the main thread is busy, as a finger's does.
const int y0 = x, y1 = y, ms = n > 0 ? n : 1;
std::thread([this, panProxy, y0, y1, ms, scrollPacket]() {
const int64_t t0 = jihadTickNowMs();
std::vector<uint8_t> buf;
for (;;) {
const int64_t el = jihadTickNowMs() - t0;
const int py = el >= ms ? y1 : (int)(y0 + (y1 - y0) * (double)el / ms);
const int len = scrollPacket(buf, py);
{
jihad::FrameLockGuard frameLock;
if (mPages.find(panProxy) == mPages.end()) break;
panProxy->injectAsync(buf.data(), len);
}
if (el >= ms) break;
usleep(16000);
}
printf("[jihad-bs] inject pan done (the card's path)\n");
}).detach();
} else {
mPanPage = p; mPanY0 = x; mPanY1 = y; mPanMs = n > 0 ? n : 1; mPanT0 = jihadTickNowMs(); mPanActive = true;
}
jihad::GpuDumpRestart(); // the frame dumps count from the pan's start
} else if (sscanf(c.c_str(), "hold %d %d", &x, &y) == 2) {
printf("[jihad-bs] inject hold %d,%d\n", x, y); p->holdAt(x, y);
@@ -509,8 +602,31 @@ void JihadBrowserServer::processInjectFile() {
printf("[jihad-bs] inject size %dx%d\n", x, y); p->setWindowSize(x, y);
} else if (c.find_first_not_of(" \t\r\n") != std::string::npos) {
double z = 1.0;
if (sscanf(c.c_str(), "zoom %lf %d %d", &z, &x, &y) == 3) p->setZoomAndScroll(z, x, y);
else printf("[jihad-bs] inject: bad cmd: %s", c.c_str());
int delayMs = 0;
if (sscanf(c.c_str(), "zoom %lf %d %d %d", &z, &x, &y, &delayMs) >= 3) {
// T-515: with the express lane, through the card's own path (the proxy's reader side), as
// a pinch arrives, on a thread after delayMs (so it lands while the page's script runs, as a
// card's command does: this file is read only when the main thread is free); else straight
// to the page as before
YapProxy* zp = nullptr;
for (auto& kv : mPages) if (kv.second.page == p) zp = kv.first;
bool sent = false;
if (jihad::jihadExpressOn() && zp) {
printf("[jihad-bs] inject zoom %.4f at %d,%d in %d ms (the card's path)\n", z, x, y, delayMs);
std::thread([this, zp, z, x, y, delayMs]() {
if (delayMs > 0) usleep((useconds_t)delayMs * 1000);
std::vector<uint8_t> buf(kMaxMsgLen, 0);
YapPacket* w = YapProxy::wrapPacket(buf.data(), 0);
(*w) << (int16_t)0x150e; (*w) << z; (*w) << (int32_t)x; (*w) << (int32_t)y;
const int len = w->length();
YapProxy::freePacket(w);
jihad::FrameLockGuard frameLock;
if (mPages.find(zp) != mPages.end()) zp->injectAsync(buf.data(), len);
}).detach();
sent = true;
}
if (!sent) p->setZoomAndScroll(z, x, y);
} else printf("[jihad-bs] inject: bad cmd: %s", c.c_str());
}
}
}
@@ -624,6 +740,7 @@ void JihadBrowserServer::tick() {
// any re-entry — the outer tick still owns this cycle. (mInTick also defers
// reap, so a connect/disconnect during nested pumping stays safe.)
if (mInTick) return;
jihad::FrameLockGuard frameLock; // T-515: let go of only around the engine's pump below
mInTick = true;
// Snapshot page pointers so a connect/disconnect during nested GLib pumping
@@ -705,9 +822,10 @@ void JihadBrowserServer::tick() {
}
for (auto* pg : snap) {
int64_t a = jihadTickNowMs();
pg->pump(4, /*drainOnly*/true);
{ jihad::FrameLockRelease unlocked; pg->pump(4, /*drainOnly*/true); } // T-515: the page's script runs here
int64_t b = jihadTickNowMs();
pumpMs += b - a;
jihad::jihadTimelineMain("pump", (long)(b - a)); // T-515 device rounds
pg->maybePaint();
paintMs += jihadTickNowMs() - b;
// Pull the next plugin frame. HERE and not inside maybePaint, because maybePaint is also
@@ -802,6 +920,11 @@ void JihadBrowserServer::asyncCmdConnect(YapProxy* proxy, int32_t pageWidth, int
printf("[jihad-bs] page init FAILED\n"); delete page; delete sink; return;
}
mPages[proxy] = Page{ page, sink };
if (jihad::jihadExpressOn()) { // T-515: the reader thread's way to the page
std::lock_guard<std::mutex> l(jihad::jihadWantLock());
if (!sExpressPages) sExpressPages = new std::map<YapProxy*, jihad::BrowserPageGoanna*>();
(*sExpressPages)[proxy] = page;
}
mConnectOrder.push_back(proxy);
mLastProxy = proxy; // newest connect = active card = self-drive inject target
if (mWakeHook) mWakeHook(); // a page exists now: leave the idle tick at once (Main.cpp)
@@ -1190,7 +1313,7 @@ void JihadBrowserServer::asyncCmdIgnoreMetaTags(YapProxy* proxy, bool ignore)
void JihadBrowserServer::asyncCmdSetScrollPosition(YapProxy* proxy, int32_t cx, int32_t cy, int32_t cw, int32_t ch)
{
(void)cw; (void)ch; // cw/ch describe the content window; scroll uses cx/cy.
if (auto* p = pageFor(proxy)) p->setScrollPosition(cx, cy);
if (auto* p = pageFor(proxy)) { p->setScrollPosition(cx, cy); p->expressCommandDone(YapProxy::currentTag()); } // T-515
}
// The confirmation leg of the fullscreen-plugin round trip: the plugin asks for fullscreen, the
@@ -1278,7 +1401,7 @@ void JihadBrowserServer::asyncCmdReturnBuffer(YapProxy* proxy, int32_t sharedBuf
void JihadBrowserServer::asyncCmdSetZoomAndScroll(YapProxy* proxy, double zoom, int32_t cx, int32_t cy)
{
if (auto* p = pageFor(proxy)) p->setZoomAndScroll(zoom, cx, cy);
if (auto* p = pageFor(proxy)) { p->setZoomAndScroll(zoom, cx, cy); p->expressCommandDone(YapProxy::currentTag()); } // T-515
}
void JihadBrowserServer::asyncCmdScrollLayer(YapProxy* proxy, int32_t id, int32_t deltaX, int32_t deltaY)
@@ -97,6 +97,12 @@ public:
// keeps the tick at the quiet period while it did, so chrome-side work (an add-on install, a
// pref flush) finishes in seconds rather than one event per idle second.
bool idleHadWork() const { return mIdleHadWork; }
// T-515: a proxy's page without the activity mark and the wake hook pageFor() has (the YAP
// reader thread's express hook; the frame lock is held by the caller)
jihad::BrowserPageGoanna* pageQuiet(YapProxy* proxy) {
auto it = mPages.find(proxy);
return it == mPages.end() ? nullptr : it->second.page;
}
void newPageRequested(YapProxy* proxy, const char* url); // window.open -> msgCreatePage
void setWakeHook(void (*hook)()) { mWakeHook = hook; }
// See the definition: a thumbnail of the page the user is in, at its next paint.
+187 -9
View File
@@ -40,6 +40,11 @@ LICENSE@@@ */
#include "YapProxy.h"
static const int kMaxConnectTries = 5;
#include <poll.h>
YapProxy::ExpressHook YapProxy::s_expressHook = 0;
YapProxy::DispatchHook YapProxy::s_dispatchHook = 0;
uint32_t YapProxy::s_currentTag = 0;
gboolean YapProxyIoFunction(GIOChannel* channel, GIOCondition condition, void* data);
@@ -82,7 +87,12 @@ YapProxy::YapProxy(YapServer* server, int cmdSocketFd, char* msgSocketPath, char
, m_packetCommand(0)
, m_packetReply(0)
, m_packetMessage(0)
, m_threaded(false)
, m_tagNext(0)
, m_wakeChannel(0)
, m_wakeSource(0)
{
m_wakePipe[0] = m_wakePipe[1] = -1;
if (msgSocketPostfix) {
int length = strlen(msgSocketPostfix);
@@ -105,7 +115,23 @@ YapProxy::YapProxy(YapServer* server, int cmdSocketFd, char* msgSocketPath, char
GMainContext* mainCtxt = g_main_loop_get_context(m_server->mainLoop());
if (m_cmdSocketFd != -1) {
if (m_cmdSocketFd != -1 && s_expressHook && ::pipe(m_wakePipe) == 0) {
// T-515: a reader thread reads the commands; the main loop wakes on the pipe
::fcntl(m_wakePipe[0], F_SETFL, ::fcntl(m_wakePipe[0], F_GETFL) | O_NONBLOCK);
m_wakeChannel = g_io_channel_unix_new(m_wakePipe[0]);
m_wakeSource = g_io_create_watch(m_wakeChannel, (GIOCondition) G_IO_IN);
g_source_set_callback(m_wakeSource, (GSourceFunc) YapProxy::wakeFunction, this, NULL);
g_source_attach(m_wakeSource, mainCtxt);
g_source_set_priority(m_wakeSource, G_PRIORITY_HIGH);
m_threaded = ::pthread_create(&m_reader, NULL, YapProxy::readerMain, this) == 0;
if (!m_threaded) {
g_source_destroy(m_wakeSource); g_source_unref(m_wakeSource); m_wakeSource = 0;
g_io_channel_unref(m_wakeChannel); m_wakeChannel = 0;
::close(m_wakePipe[0]); ::close(m_wakePipe[1]); m_wakePipe[0] = m_wakePipe[1] = -1;
}
fprintf(stderr, "[jihad-bs] YAP commands read on %s\n", m_threaded ? "a reader thread (the express lane)" : "the main loop (no thread)");
}
if (m_cmdSocketFd != -1 && !m_threaded) {
m_ioChannel = g_io_channel_unix_new(m_cmdSocketFd);
m_ioSource = g_io_create_watch(m_ioChannel, (GIOCondition) (G_IO_IN | G_IO_HUP));
@@ -165,6 +191,26 @@ YapProxy::~YapProxy()
delete[] m_msgSocketPostfix; // allocated with new char[length+1] — must be delete[] (was UB)
m_msgSocketPostfix = 0;
if (m_threaded) {
// T-515: the reader's blocked read returns once the socket is shut down. Never called
// with the server's lock held: the reader may be waiting for it in the express hook.
if (m_cmdSocketFd != -1) ::shutdown(m_cmdSocketFd, SHUT_RDWR);
::pthread_join(m_reader, NULL);
m_threaded = false;
}
if (m_wakeSource) {
g_source_destroy(m_wakeSource);
g_source_unref(m_wakeSource);
m_wakeSource = 0;
}
if (m_wakeChannel) {
g_io_channel_unref(m_wakeChannel);
m_wakeChannel = 0;
}
if (m_wakePipe[0] != -1) ::close(m_wakePipe[0]);
if (m_wakePipe[1] != -1) ::close(m_wakePipe[1]);
while (!m_q.empty()) { delete m_q.front(); m_q.pop_front(); }
if (m_ioSource) {
g_source_destroy(m_ioSource);
}
@@ -234,6 +280,7 @@ void* YapProxy::privateData() const
YapPacket* YapProxy::packetMessage()
{
m_sendLock.lock(); // T-515: released by sendMessage(), which every caller makes next
m_packetMessage->reset();
return m_packetMessage;
}
@@ -245,6 +292,7 @@ void YapProxy::setTerminate()
void YapProxy::sendMessage()
{
std::lock_guard<std::mutex> sendGuard(m_sendLock, std::adopt_lock); // T-515: taken in packetMessage()
char pktHeader[4];
uint16_t pktLen = 0;
@@ -304,6 +352,22 @@ void YapProxy::ioFunction(GIOChannel* channel, GIOCondition condition)
goto Detached;
}
if (handlePacket(pktFlags, pktLen))
return;
Detached:
m_server->clientDisconnected(this);
delete this;
}
// T-515: the command now in m_cmdBuffer (pktLenIn bytes), handled as ioFunction always did; false
// when the proxy must detach (the caller detaches it).
bool YapProxy::handlePacket(uint8_t pktFlags, int pktLenIn)
{
char pktHeader[4] = { 0, 0, 0, 0 };
uint16_t pktLen = (uint16_t) pktLenIn;
m_inSyncMode = pktFlags & kPacketFlagSyncMask;
if (m_inSyncMode) {
@@ -320,12 +384,12 @@ void YapProxy::ioFunction(GIOChannel* channel, GIOCondition condition)
pktHeader[3] = 0;
if (!writeSocket(m_cmdSocketFd, pktHeader, 4)) {
goto Detached;
return false;
}
if (m_packetReply->length() > 0) {
if (!writeSocket(m_cmdSocketFd, (char*) m_replyBuffer, m_packetReply->length())) {
goto Detached;
return false;
}
}
@@ -345,16 +409,130 @@ void YapProxy::ioFunction(GIOChannel* channel, GIOCondition condition)
m_inSyncMode = false;
if (m_terminate) {
goto Detached;
return !m_terminate;
}
// T-515: the reader thread. Each command is copied into the queue; an async one is offered to the
// express hook first (a scroll, a zoom, a buffer returned: the express lane may answer them while
// the main thread is busy). A closed socket ends the thread with a hang-up entry.
void* YapProxy::readerMain(void* self)
{
static_cast<YapProxy*>(self)->readerLoop();
return NULL;
}
bool YapProxy::readWait(int fd, char* buf, int len)
{
int index = 0;
while (len > 0) {
int count = ::read(fd, &buf[index], len);
if (count > 0) { index += count; len -= count; continue; }
if (count == 0) return false; // closed
if (errno == EINTR) continue;
if (errno == EAGAIN) {
struct pollfd pfd = { fd, POLLIN, 0 };
::poll(&pfd, 1, 1000);
continue;
}
return false;
}
return true;
}
return;
void YapProxy::readerLoop()
{
for (;;) {
char hdr[4];
if (!readWait(m_cmdSocketFd, hdr, 4)) break;
uint16_t len;
::memcpy(&len, hdr, 2);
len = bswap_16(len);
if (len > kMaxMsgLen) {
fprintf(stderr, "YAP: Invalid message length %u > %d\n", len, kMaxMsgLen);
break;
}
InPacket* p = new InPacket;
p->flags = (uint8_t) hdr[3]; p->hup = false; p->tag = ++m_tagNext;
p->data.resize(len);
if (len && !readWait(m_cmdSocketFd, (char*) &p->data[0], len)) { delete p; break; }
if (!(p->flags & kPacketFlagSyncMask) && s_expressHook && s_expressHook(this, len ? &p->data[0] : NULL, len, p->tag)) {
delete p; // the express lane applied it
continue;
}
{ std::lock_guard<std::mutex> g(m_qLock); m_q.push_back(p); }
char b = 1;
ssize_t w = ::write(m_wakePipe[1], &b, 1); (void) w;
}
InPacket* p = new InPacket;
p->flags = 0; p->hup = true; p->tag = 0;
{ std::lock_guard<std::mutex> g(m_qLock); m_q.push_back(p); }
char b = 1;
ssize_t w = ::write(m_wakePipe[1], &b, 1); (void) w;
}
Detached:
YapPacket* YapProxy::wrapPacket(uint8_t* data, int len)
{
return len > 0 ? new YapPacket(data, len) : new YapPacket(data);
}
m_server->clientDisconnected(this);
delete this;
void YapProxy::freePacket(YapPacket* pkt)
{
delete pkt;
}
bool YapProxy::injectAsync(const uint8_t* data, int len)
{
if (!m_threaded || len <= 0 || len > kMaxMsgLen) return false;
const uint32_t tag = ++m_tagNext;
if (s_expressHook && s_expressHook(this, data, len, tag)) return true;
InPacket* p = new InPacket;
p->flags = 0; p->hup = false; p->tag = tag;
p->data.assign(data, data + len);
{ std::lock_guard<std::mutex> g(m_qLock); m_q.push_back(p); }
char b = 1;
ssize_t w = ::write(m_wakePipe[1], &b, 1); (void) w;
return true;
}
gboolean YapProxy::wakeFunction(GIOChannel*, GIOCondition, void* data)
{
YapProxy* self = static_cast<YapProxy*>(data);
char buf[64];
while (::read(self->m_wakePipe[0], buf, sizeof(buf)) > 0) {}
self->drainQueue(); // may delete self
return TRUE;
}
// The main loop's side: the queued commands handled in order, each inside the dispatch hook (the
// server's lock). A detach (a hang-up, a failed reply, a terminate) runs inside the hook too, and
// the proxy is deleted after it, outside the lock (the destructor joins the reader).
void YapProxy::drainQueue()
{
for (;;) {
InPacket* p = 0;
{
std::lock_guard<std::mutex> g(m_qLock);
if (m_q.empty()) return;
p = m_q.front();
m_q.pop_front();
}
bool alive = false;
if (s_dispatchHook) s_dispatchHook(true);
if (!p->hup) {
const int len = (int) p->data.size();
if (len) ::memcpy(m_cmdBuffer, &p->data[0], len);
s_currentTag = p->tag;
alive = handlePacket(p->flags, len);
s_currentTag = 0;
}
delete p;
if (!alive) m_server->clientDisconnected(this);
if (s_dispatchHook) s_dispatchHook(false);
if (!alive) {
delete this;
return;
}
}
}
bool YapProxy::readSocket(int fd, char* buf, int len)
+43
View File
@@ -22,6 +22,11 @@ LICENSE@@@ */
#include <stdint.h>
#include <glib.h>
#include <queue>
#include <deque>
#include <vector>
#include <mutex>
#include <atomic>
#include <pthread.h>
class YapServer;
class YapPacket;
@@ -29,6 +34,18 @@ class YapPacket;
class YapProxy
{
public:
// Jihad T-515, the express lane: when the server sets the express hook, each proxy reads its
// command socket on a thread of its own and the main loop handles the commands from a queue,
// as it handled them from the socket before. The hook sees every async command first, on the
// reader thread; true means it applied the command and the main loop must not see it. The
// dispatch hook runs around the main loop's handling of each command (the server's lock).
// tag: the command's number in this proxy's arrival order; the main loop's handling of it can
// read it back with currentTag()
typedef bool (*ExpressHook)(YapProxy* proxy, const uint8_t* data, int len, uint32_t tag);
static uint32_t currentTag() { return s_currentTag; }
typedef void (*DispatchHook)(bool begin);
static ExpressHook s_expressHook;
static DispatchHook s_dispatchHook;
const char* postfix() const { return m_msgSocketPostfix; }
void setPrivateData(void* priv);
@@ -37,6 +54,14 @@ public:
bool isRecordProxy() const;
YapPacket* packetMessage();
// T-515, device rounds (the inject channel's pan): an async command handed in as the reader
// thread would hand it in (the express hook, then the queue), from any thread; false without
// the reader thread
bool injectAsync(const uint8_t* data, int len);
// T-515: a packet over the caller's bytes (YapPacket's constructors are the proxy's): for
// reading len bytes, or for writing (len 0, the buffer kMaxMsgLen long); freed with freePacket
static YapPacket* wrapPacket(uint8_t* data, int len);
static void freePacket(YapPacket* pkt);
void sendMessage();
void transferQueuedMessage(YapProxy* srcProxy);
@@ -66,6 +91,24 @@ private:
~YapProxy();
void ioFunction(GIOChannel* channel, GIOCondition condition);
// T-515: the reader thread, the queue and the main loop's side of it
struct InPacket { std::vector<uint8_t> data; uint8_t flags; bool hup; uint32_t tag; };
std::atomic<uint32_t> m_tagNext;
static uint32_t s_currentTag; // the main loop's command being handled (its thread only)
static void* readerMain(void* self);
void readerLoop();
bool readWait(int fd, char* buf, int len);
static gboolean wakeFunction(GIOChannel* channel, GIOCondition condition, void* data);
void drainQueue();
bool handlePacket(uint8_t flags, int len); // the command in m_cmdBuffer; false: the proxy is gone
bool m_threaded;
pthread_t m_reader;
int m_wakePipe[2];
GIOChannel* m_wakeChannel;
GSource* m_wakeSource;
std::mutex m_qLock;
std::deque<InPacket*> m_q;
std::mutex m_sendLock; // packetMessage() .. sendMessage(): the main thread and the express lane
bool readSocket(int fd, char* buf, int len);
bool writeSocket(int fd, char* buf, int len);
+525 -13
View File
@@ -16,6 +16,7 @@ extern bool gJihadViewportFollow; // EngineHost.cpp: jihad.viewport.followOver
#include "JihadRuntimePaths.h" // the ONE runtime-state dir (T-057 / R8)
#include "JihadCrashGuard.h" // do not reopen a page that just killed the daemon twice
#include "JihadCertStore.h" // webOS platform certificate store (R5)
#include "JihadFrameLock.h" // T-515: the express lane's frame lock
#include <cstdio>
#include <sys/time.h> // gettimeofday (paint render timing)
@@ -37,6 +38,11 @@ extern bool gJihadViewportFollow; // EngineHost.cpp: jihad.viewport.followOver
#include <cstring>
#include <algorithm> // std::remove (live-page registry)
#include <vector>
#include <climits> // LONG_MAX
#include <set> // T-515: the express lane's pages
#include <thread>
#include <condition_variable>
#include <chrono>
namespace { } // (UrlRule defined below in the jihad namespace)
@@ -193,6 +199,11 @@ static void jihadStartAudioPoll() {
if (pthread_create(&t, nullptr, jihadAudioPollThread, nullptr) == 0) { pthread_detach(t); }
}
// T-515: the express lane's pages (under the frame lock) and its thread (defined further down)
static std::set<BrowserPageGoanna*>* gXPages = nullptr;
static std::thread* gXThread = nullptr; // under the frame lock
static void jihadExpressThread();
BrowserPageGoanna::BrowserPageGoanna(EngineHost& host, IPageMessageSink& sink)
: mHost(host), mSink(sink), mPage(nullptr),
mKey1(0), mKey2(0), mBufSize(0), mActiveKey(0),
@@ -219,6 +230,16 @@ BrowserPageGoanna::BrowserPageGoanna(EngineHost& host, IPageMessageSink& sink)
// this nothing implements DialogSink and every dialog silently takes its default.
jihadLivePages().push_back(this);
SetDialogSink(this);
if (jihadExpressOn()) { // T-515
FrameLockGuard g;
if (!gXPages) {
gXPages = new std::set<BrowserPageGoanna*>();
gXThread = new std::thread(jihadExpressThread);
fprintf(stderr, "[jihad-gpu] express lane on: frames while the main thread is busy (after %s ms)\n",
getenv("JIHAD_GPU_EXPRESS_MS") ? getenv("JIHAD_GPU_EXPRESS_MS") : "45");
}
gXPages->insert(this);
}
// Session certificates a previous daemon died holding are removed here, before
// this page can install any of its own. It is idempotent and runs on the first
// page ONLY, which is the earliest point this file owns; a daemon that never
@@ -341,6 +362,74 @@ static int64_t jihadNowMs() {
return (int64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
// T-515, the express lane: frames composed and published by a thread of their own while the main
// thread is busy in the engine (JihadFrameLock.h says why). JIHAD_GPU_EXPRESS=1 with the GPU path
// (JIHAD_GPU_COMPOSITOR); JIHAD_GPU_EXPRESS_MS (default 45) is how long the card waits for the main
// lane's frame before the express lane makes one.
// T-515 device rounds (JIHAD_GPU_TIMELINE=1): one line per published frame, either lane, and per
// long engine call of the main thread, on one clock ("[jihad-tl] t=<ms>"), to see where gaps come from
bool jihadTimelineOn() {
static const bool sOn = getenv("JIHAD_GPU_TIMELINE") != nullptr;
return sOn;
}
static int64_t gJihadTlLastPub = 0;
static void jihadTimelinePublish(const char* lane, int64_t start, int y, double zoom) {
if (!jihadTimelineOn()) return;
const int64_t now = jihadNowMs();
fprintf(stderr, "[jihad-tl] t=%lld publish %s gap=%ld made=%ld y=%d zoom=%.4f\n", (long long)now, lane,
gJihadTlLastPub ? (long)(now - gJihadTlLastPub) : -1L, start ? (long)(now - start) : -1L, y, zoom);
gJihadTlLastPub = now;
}
void jihadTimelineMain(const char* what, long ms) { // ms: how long it took, ending now
if (!jihadTimelineOn() || ms < 15) return;
fprintf(stderr, "[jihad-tl] t=%lld main %s %ld ms\n", (long long)jihadNowMs(), what, ms);
}
bool jihadExpressOn() {
static const bool sOn = getenv("JIHAD_GPU_EXPRESS") && atoi(getenv("JIHAD_GPU_EXPRESS")) != 0 &&
getenv("JIHAD_GPU_COMPOSITOR") != nullptr;
return sOn;
}
namespace {
std::mutex gXWakeLock;
std::condition_variable gXWake;
bool gXKicked = false;
bool gXQuit = false; // under gXWakeLock
void jihadExpressKick() {
{ std::lock_guard<std::mutex> l(gXWakeLock); gXKicked = true; }
gXWake.notify_one();
}
// Each step under the frame lock, short (a decision, a post, a poll, a publish); between steps the
// thread sleeps without it: 3 ms while a page has a frame in flight or a card waiting, else until
// the next want.
} // namespace
static void jihadExpressThread() {
bool soon = false;
for (;;) {
{
std::unique_lock<std::mutex> l(gXWakeLock);
if (!gXKicked && !gXQuit) gXWake.wait_for(l, std::chrono::milliseconds(soon ? 3 : 1000));
gXKicked = false;
if (gXQuit) return;
}
FrameLockGuard g;
soon = false;
const int64_t now = jihadNowMs();
if (gXPages) for (BrowserPageGoanna* p : *gXPages) soon = p->expressStep(now) || soon;
}
}
// The daemon's exit: the thread ends before the statics it uses go away.
void jihadExpressStop() {
std::thread* t;
{ FrameLockGuard g; t = gXThread; gXThread = nullptr; }
if (!t) return;
{ std::lock_guard<std::mutex> l(gXWakeLock); gXQuit = true; }
gXWake.notify_one();
t->join();
delete t;
}
// The address as it may go to the card: at most 8 KiB. A page can make its own URL as long as it
// likes (history.replaceState with a 20 000-character query) and the YAP packet is a fixed
// 16 KiB, in which an oversized field is skipped while the ones after it are still written —
@@ -419,6 +508,8 @@ int BrowserPageGoanna::slotForKey(int key) const {
// completed paints stayed pinned at ~26/s).
void BrowserPageGoanna::setExtraBuffer(int key3, int size) {
if (!key3) return;
FrameLockGuard frameLock; // T-515
xInvalidate();
// Must match the pair we already have, or the rotation would hand the adapter a buffer of a
// different size than the header it reads describes.
if (size > 0 && mBufSize > 0 && size != mBufSize) {
@@ -476,7 +567,9 @@ unsigned char* BrowserPageGoanna::attachShm(int keyOrId) {
}
void BrowserPageGoanna::detachShm() {
gpuJobDrain(); // T-502
FrameLockGuard frameLock; // T-515
gpuJobDrain(); // T-502 (and the express lane's frame, T-515)
xInvalidate();
for (int i = 0; i < 3; ++i) { if (mShmBuf[i]) shmdt(mShmBuf[i]); mShmBuf[i] = nullptr; mShmId[i] = -1; }
}
@@ -529,6 +622,9 @@ bool BrowserPageGoanna::writeThumbnail(const unsigned char* src, int w, int h, c
}
BrowserPageGoanna::~BrowserPageGoanna() {
// T-515: out of the express lane's reach first; its frame in flight is waited out with the jobs
FrameLockGuard frameLock;
if (gXPages) gXPages->erase(this);
// Never leave the process-wide sink pointing at a destroyed page — and never take it away
// from a page that is still alive. If this page owned the dialogs they pass to the newest
// remaining one; if it did not (an older card closing while a newer one is open), nothing
@@ -624,6 +720,8 @@ bool BrowserPageGoanna::init(uint32_t width, uint32_t height,
}
void BrowserPageGoanna::setWindowSize(uint32_t width, uint32_t height) {
FrameLockGuard frameLock; // T-515
xInvalidate();
mGpuZoom = 0.0; // T-502: the scroll port follows the card size; set again at the next zoom
mGpuAnchorValid = false;
mLayoutFollowed = false; // T-499: a new card size (rotation) starts from a layout viewport equal to it
@@ -738,6 +836,7 @@ void BrowserPageGoanna::freeze() {
// Drop the cached shm attachments too: the production segments are IPC_RMID-marked,
// so holding an attach keeps them alive and can exhaust the device's scarce shared
// memory while backgrounded. thaw() re-validates and maybePaint() re-attaches on demand.
FrameLockGuard frameLock; // T-515
mFrozen = true;
if (mPage) mPage->SetActive(false); // a background card is a background tab: timers clamped, no frames
detachShm();
@@ -754,7 +853,9 @@ void BrowserPageGoanna::thaw(int key1, int key2, int size) {
// keys may be SHMIDs (isis) or ftok keys (test adapter) &mdash; validate via the resolver.
if (ok) for (int k : { key1, key2 }) { if (k && jihadShmResolve(k) < 0) ok = false; }
if (!ok) return; // remain frozen; keep the old (already-detached) keys inactive
FrameLockGuard frameLock; // T-515
detachShm(); // the buffers may be new segments — drop stale cached attachments
mXConsumed[0] = mXConsumed[1] = mXConsumed[2] = 0;
mKey1 = key1; mKey2 = key2; mBufSize = size; mActiveKey = mKey1;
mKey3 = 0; // re-announced by the adapter after thaw; the old third segment may be gone
mInFlight[0] = mInFlight[1] = mInFlight[2] = false; // reattached (possibly new) segments: clear in flight (F-211)
@@ -785,7 +886,8 @@ void BrowserPageGoanna::returnBuffer(int sharedBufferKey) {
// adapter (in LunaSysMgr) crashed. If a queued paint was deferred waiting on this return,
// paint it now so the latest frame lands promptly instead of on the next tick.
int s = slotForKey(sharedBufferKey);
if (s >= 0) mInFlight[s] = false;
if (s >= 0 && mXConsumed[s] > 0) --mXConsumed[s]; // T-515: the express hook applied this return already
else if (s >= 0) mInFlight[s] = false;
if (mNeedsPaint && !mFrozen) maybePaint();
}
@@ -804,6 +906,7 @@ void BrowserPageGoanna::returnBuffer(int sharedBufferKey) {
// scroll on back/forward either (restoreFrameStateRequested is an empty stub), so calling this
// from all five nav commands reproduces stock exactly.
void BrowserPageGoanna::resetScrollForNavigation() {
FrameLockGuard frameLock; // T-515: called from the pump too (a page's own navigation)
mLayoutFollowed = false; // T-499: the next page decides its own layout viewport
// A new document invalidates both buffers wholesale. The damage-only path proves its
// premise from geometry (size, zoom, scroll), and a navigation can leave every one of
@@ -977,6 +1080,8 @@ void MarkInputLatency() { gJihadInputMarkMs = jihadNowMs(); }
// scroll, view, overscan and resolution back to neutral, the next paint in software
void BrowserPageGoanna::gpuFallBack(const char* why) {
if (!mPage || !mPage->GpuPath()) return;
FrameLockGuard frameLock; // T-515
xInvalidate();
for (const GpuJob& j : mGpuJobs) mPage->ComposeForget(j.token);
mGpuJobs.clear();
for (int i = 0; i < 3; ++i) mGpuSlotValid[i] = false;
@@ -3182,6 +3287,10 @@ void BrowserPageGoanna::pump(int msBudget, bool drainOnly) {
// not returned the buffer, and frames actually painted.
void BrowserPageGoanna::maybePaint() {
if (!mGpuJobs.empty()) mGpuTrace[6]++;
// T-515: an express frame at the held zoom is out (the main lane's own may be dropped for as long
// as the page's script runs): the displayport and the anchor follow the zoom now
if (mGpuZoomHoldMs && mXLastZoom > 0.0 && fabs(mXLastZoom - mGpuZoomHoldZ) < 1e-9)
gpuZoomRelease("the first frame at the new zoom is out (the express lane)");
{
// T-502 device rounds: the cadence this page's paint driver is actually called at
static int64_t sPrev = 0; static int sLogged = 0;
@@ -3391,6 +3500,14 @@ static bool jihadPluginPullEnabled() {
// mapping goes away (a reattach, a resize) the job is waited out (3 s at most) and forgotten.
// Every slot's record is dropped with it: the composite's damage went unrecorded.
void BrowserPageGoanna::gpuJobDrain() {
FrameLockGuard frameLock; // T-515
for (XJob* xj : { &mXJob2, &mXJob }) {
if (!xj->on || !mPage) continue;
// the express lane's frames: the compositor writes into their slots until they are done
const int64_t t0 = jihadNowMs();
while (mPage->ComposePoll(xj->token, nullptr, nullptr) <= -2 && jihadNowMs() - t0 < 3000) usleep(2000);
xForget(*xj);
}
if (mGpuJobs.empty()) return;
{
static uint32_t sN = 0;
@@ -3535,7 +3652,7 @@ bool BrowserPageGoanna::gpuJobStart(int slot, int key, unsigned char* pixels, si
if (iv[p2][0] <= iv[q][1] && iv[q][0] <= iv[p2][1]) { iv[p2][0] = std::min(iv[p2][0], iv[q][0]); iv[p2][1] = std::max(iv[p2][1], iv[q][1]); iv[q][0] = iv[n-1][0]; iv[q][1] = iv[n-1][1]; n--; merged = true; }
}
while (n > 2) {
int bi = 0, bj = 1; long best = 1L << 40;
int bi = 0, bj = 1; long best = LONG_MAX; // was 1L << 40: past a 32-bit long (ARM)
for (int p2 = 0; p2 < n; p2++) for (int q = p2 + 1; q < n; q++) { const long gap = std::max(iv[p2][0], iv[q][0]) - std::min(iv[p2][1], iv[q][1]); if (gap < best) { best = gap; bi = p2; bj = q; } }
iv[bi][0] = std::min(iv[bi][0], iv[bj][0]); iv[bi][1] = std::max(iv[bi][1], iv[bj][1]); iv[bj][0] = iv[n-1][0]; iv[bj][1] = iv[n-1][1]; n--;
}
@@ -3575,8 +3692,12 @@ bool BrowserPageGoanna::gpuJobStart(int slot, int key, unsigned char* pixels, si
// the diff's shift is exact when the base is the previous composite: the newest finished frame
// for a first job, the job in flight for a second (round 130)
const bool baseIsPrevious = src != nullptr && (!mGpuNextSecond || mGpuNextBaseKey != 0);
const uint32_t token = mPage->ComposeStart(pixels, bytes, sLo, sHi, s2Lo, s2Hi, shift, src,
srcYForEngine, baseIsPrevious);
// T-515: the layers painted already (the main lane's paint window, the frame lock let go of): the
// compose posted on its own
const uint32_t token = mGpuPainted
? mPage->ComposePost(pixels, bytes, h, sLo, sHi, s2Lo, s2Hi, shift, src, srcYForEngine, baseIsPrevious, nullptr)
: mPage->ComposeStart(pixels, bytes, sLo, sHi, s2Lo, s2Hi, shift, src, srcYForEngine, baseIsPrevious);
if (token) mXLastPostToken = token;
if (!token) {
// T-509: a compositor that takes no job (no GL context: EGL failed) — three in a row and the
// daemon renders in software for good
@@ -3587,6 +3708,8 @@ bool BrowserPageGoanna::gpuJobStart(int slot, int key, unsigned char* pixels, si
j.token = token; j.slot = slot; j.key = key; j.x = x; j.y = y; j.zoom = zoom; j.w = w; j.h = h;
j.t0 = jihadNowMs(); j.seq = mPage->CompositorSeq(); j.askLo = sLo; j.askHi = sHi; j.ask2Lo = s2Lo; j.ask2Hi = s2Hi; j.shift = shift;
j.viewRow = mGpuViewRow;
j.xFrames = mXFrames;
j.cmdDone = mXCmdDone;
mGpuJobs.push_back(j);
return true;
}
@@ -3629,6 +3752,312 @@ void BrowserPageGoanna::gpuJobStartNext(int x, int y, double zoom, int w, int h,
}
}
// T-515, the express lane (JihadFrameLock.h). The main lane makes the frames as before; when the
// card has waited JIHAD_GPU_EXPRESS_MS (45) for one and the main lane has published nothing for as
// long (its thread is in the page's script, or paints a displayport move), the express thread
// composes the layer tree as it stands at the card's newest scroll and zoom, from its own reading
// of the card's commands (the YAP reader thread), and publishes it. Its frames build on the newest
// composite when that is intact in a slot (the shift and the bands as the main lane's "newest
// slot" rule), else they are read whole; the main lane's next frame builds on the express lane's.
// What it does not do: move the engine (the anchor, the displayport, the resolution: the main
// thread's), draw popups, or make frames for anything but the card's moves.
// The geometry and the buffers of the main lane's GPU frame (its view's overscan included).
void BrowserPageGoanna::xSnapshot(int w, int h, int frameH, int os, size_t hdr) {
XSnap& S = mXSnap;
const int keys[3] = { mKey1, mKey2, mKey3 };
if (!S.ok || S.w != w || S.h != h || S.frameH != frameH || S.os != os ||
S.keys[0] != keys[0] || S.keys[1] != keys[1] || S.keys[2] != keys[2]) {
// new geometry or buffers: a frame in flight and the base are of the old ones, and the main
// lane's first frame of them comes first
++mXGeomGen; mXPub = XPub(); mXMainPubMs = 0;
for (int i = 0; i < 3; ++i) {
S.keys[i] = keys[i]; S.base[i] = nullptr; S.seg[i] = 0;
if (!keys[i]) continue;
const int id = jihadShmResolve(keys[i]);
struct shmid_ds ds;
if (id < 0 || shmctl(id, IPC_STAT, &ds) != 0) continue;
S.seg[i] = ds.shm_segsz; S.base[i] = attachShm(keys[i]);
}
}
S.w = w; S.h = h; S.frameH = frameH; S.os = os; S.hdr = hdr; S.contentH = mLastContentH;
S.ok = true;
}
void BrowserPageGoanna::expressDetach() {
if (!jihadExpressOn()) return;
FrameLockGuard frameLock;
if (gXPages) gXPages->erase(this);
gpuJobDrain(); // the express lane's frames too: the compositor writes into their slots until done
}
// The YAP reader thread, the want lock held: a scroll (hasZoom false: the zoom the card last asked
// for) or a zoom. Only the want's fields: the frame lock is not held.
void BrowserPageGoanna::expressWant(int x, int y, double zoom, bool hasZoom, uint32_t tag) {
const int64_t now = jihadNowMs();
// the card following a scroll the page made (our own msgScrolledTo, setScrollPosition's rule): not
// a move (the echo's fields are the main thread's, read racily: a heuristic)
if (!hasZoom && now - mGpuEchoMs < 1500 && std::abs(x - mGpuEchoX) <= 2 && std::abs(y - mGpuEchoY) <= 2) return;
double z = hasZoom ? zoom : (mXWant.seq ? mXWant.zoom : mXZoomNow);
if (!(z >= 0.05 && z <= 20.0)) z = (mXZoomNow >= 0.05 && mXZoomNow <= 20.0) ? mXZoomNow : 1.0;
if (hasZoom && z != mXWant.zoom) { mXZoomWantMs = now; ++mXZoomDumpN; }
mXWant.x = x; mXWant.y = y; mXWant.zoom = z; mXWant.ms = now; ++mXWant.seq; mXWant.cmdSeq = tag;
if (!mXWaitSinceMs) mXWaitSinceMs = now;
jihadExpressKick();
}
// The YAP reader thread: a buffer the adapter returned, free for the express lane at once
// (returnBuffer, on the main loop later, skips the clear it already made).
void BrowserPageGoanna::expressReturn(int sharedBufferKey) {
const int s = slotForKey(sharedBufferKey);
if (s >= 0 && mInFlight[s]) { mInFlight[s] = false; ++mXConsumed[s]; jihadExpressKick(); }
}
// While the card moves (its last scroll or zoom within JIHAD_GPU_EXPRESS_LEAD_MS, 120), the express
// lane makes every frame, the main lane none: the express thread polls its frame every 3 ms and
// publishes at once, where the main lane's frames wait for its tick (the publish chain cost ~10 ms a
// frame on desktop Wikipedia) and for the page's script. The main lane keeps the engine half: the
// card's commands (the anchor, the zoom), the layer paint. 0 turns the lead off (the express lane
// then only fills the main thread's stalls).
bool BrowserPageGoanna::xLeads(int64_t now) {
static const long sLead = getenv("JIHAD_GPU_EXPRESS_LEAD_MS") ? atol(getenv("JIHAD_GPU_EXPRESS_LEAD_MS")) : 120;
if (sLead <= 0 || !mPage || !mPage->GpuPath() || !mPage->ExpressReady() || mFrozen || !mXSnap.ok || mXSnap.popups ||
mXFailures >= 3 || !mXMainPubMs)
return false;
int64_t wantMs;
{ std::lock_guard<std::mutex> l(jihadWantLock()); wantMs = mXWant.seq ? mXWant.ms : 0; }
return wantMs && now - wantMs < sLead;
}
bool BrowserPageGoanna::expressStep(int64_t now) {
if (mXJob.on) {
xPoll(now);
// the next frame while this one is read (handed to the read thread: a whole read), built on it,
// while the card moves (the main lane's round 130 rule). Posted earlier, right behind it, the
// frames came no faster on desktop Wikipedia (the compositor thread's ~45 ms a frame is the
// floor) and each was older when it came (round 266): JIHAD_GPU_EXPRESS_EARLY=1 does that.
static const bool sEarly = getenv("JIHAD_GPU_EXPRESS_EARLY") != nullptr;
if (mXJob.on && (mXJob.composed || sEarly) && !mXJob2.on && mXJob.gen == mXGeomGen && xLeads(now)) {
XWant want; int64_t waitSince;
{ std::lock_guard<std::mutex> l(jihadWantLock()); want = mXWant; waitSince = mXWaitSinceMs; }
const double Z = (want.zoom >= 0.99 && want.zoom <= 1.01) ? 1.0 : want.zoom;
if (want.seq && !(mXJob.x == want.x && mXJob.cardY == want.y && mXJob.zoom == Z))
xStart(now, Z, want, waitSince, &mXJob);
}
return true;
}
if (!mPage || !mPage->GpuPath() || !mPage->ExpressReady() || mFrozen || !mXSnap.ok || mXSnap.popups || mXFailures >= 3)
return false;
XWant want; int64_t waitSince;
{ std::lock_guard<std::mutex> l(jihadWantLock()); want = mXWant; waitSince = mXWaitSinceMs; }
if (!want.seq) return false;
const double Z = (want.zoom >= 0.99 && want.zoom <= 1.01) ? 1.0 : want.zoom; // the paint path's rule
// served: the express lane's newest frame is at the card's position, or the main lane's newest
// frame took up the card's newest command (its position is the engine's: clamped, rounded)
if (mXLastXPubMs ? (mXPub.slot >= 0 && mXPub.cardX == want.x && mXPub.cardY == want.y && mXPub.zoom == Z)
: (mXPub.slot >= 0 && mXCmdPub >= want.cmdSeq))
return false;
if (!waitSince) return false; // the main lane published since the card's last move
if (!mXMainPubMs) return false; // the main lane's first frame of this geometry and document is not out
// the main lane's frames in flight: they serve the card while the compositor has them; once
// composed, the express lane's frame may follow (the main lane drops them as older at its poll:
// its thread can sit in the page's script a long while before it looks)
for (const GpuJob& j : mGpuJobs) if (mPage->ComposePeek(j.token) == 0) return true;
if (xLeads(now)) return xStart(now, Z, want, waitSince); // the card is moving: every frame
static const long sGrace = getenv("JIHAD_GPU_EXPRESS_MS") ? atol(getenv("JIHAD_GPU_EXPRESS_MS")) : 45;
if (now - waitSince < sGrace || now - mXMainPubMs < sGrace) return true;
// only while the main thread is away in the engine (its pump, a layer paint) for a while: back
// in its tick (or idle between ticks) it makes the frame itself, and the express lane, stepping
// every 3 ms, would otherwise take every frame from it (its frames do the engine's half: the
// anchor, the zoom's release)
static const long sAway = getenv("JIHAD_GPU_EXPRESS_AWAY_MS") ? atol(getenv("JIHAD_GPU_EXPRESS_AWAY_MS")) : 15;
const int64_t away = jihadFrameLock().awaySinceMs();
if (!away || now - away < sAway) return true;
return xStart(now, Z, want, waitSince);
}
// aBase: the express frame in flight, composed: the compositor thread takes the jobs in order, so
// its frame is complete when this one is read (the main lane's round 130 rule), and this one reads
// the rows it changed too (round 116). Else the base is the newest composite, intact in its slot.
bool BrowserPageGoanna::xStart(int64_t now, double Z, const XWant& want, int64_t waitSince, const XJob* aBase) {
const XSnap& S = mXSnap;
const int w = S.w, H = S.frameH, os = S.os;
const int x = want.x, y = want.y - os; // the frame's top: the view sits os rows down it (T-505)
const size_t need = S.hdr + (size_t)w * H * 4;
// the base: the newest composite, intact in its slot, at this x, zoom and size (the engine's
// diff is against it, so the shift is exact: the main lane's round 115 rule)
const int b = aBase ? aBase->slot : mXPub.slot;
const int baseY = aBase ? (aBase->composed ? aBase->composedY : aBase->y) : mXPub.y;
bool base;
if (aBase) {
base = b >= 0 && b < 3 && aBase->x == x && aBase->zoom == Z && aBase->w == w && aBase->h == H &&
mXPub.fixedHi >= mXPub.fixedLo && aBase->token == mXLastPostToken;
} else {
base = b >= 0 && b < 3 && mXPub.token && mXPub.token == mXLastPostToken && mXPub.x == x && mXPub.zoom == Z &&
mXPub.w == w && mXPub.h == H && mXPub.fixedHi >= mXPub.fixedLo && S.base[b] && mXPub.key == S.keys[b];
}
const long dy = base ? (long)y - baseY : 0;
if (base && !(dy > -H && dy < H)) base = false;
if (aBase && !base) return false; // the second frame only builds on the first
// the slot: not the adapter's, not the main lane's paint's, not the base; the active key's only
// when no other is free (the main lane's next frame goes there)
int slot = -1;
for (int pass = 0; pass < 2 && slot < 0; ++pass) {
for (int t = 0; t < 3 && slot < 0; ++t) {
if (!S.keys[t] || !S.base[t] || S.seg[t] < need) continue;
if (mInFlight[t] && now - mPaintMs[t] < 2000) continue;
if (t == mXMainSlot || (base && t == b) || (mXJob.on && t == mXJob.slot)) continue;
if (S.keys[t] == mActiveKey && (pass == 0 || mXMainWindow)) continue;
slot = t;
}
}
if (slot < 0) return true; // a return frees one (the hook kicks)
long lo = 0, hi = H, lo2 = 0, hi2 = 0;
const unsigned char* src = nullptr;
long srcY = 0;
if (base) {
src = S.base[b] + S.hdr; srcY = baseY;
lo = dy > 0 ? H - dy : 0; hi = dy > 0 ? H : (dy < 0 ? -dy : 0); // what the shift brings in
static const bool sFixedCols = !getenv("JIHAD_GPU_NO_FIXED_COLS") && !getenv("JIHAD_GPU_READ_THREAD");
if (!sFixedCols && dy != 0 && mXPub.fixedHi > mXPub.fixedLo) {
// the rows the shift moved the fixed pixels into (round 134)
const long fLo = std::max(0L, mXPub.fixedLo - (dy > 0 ? dy : 0)), fHi = std::min((long)H, mXPub.fixedHi + (dy < 0 ? -dy : 0));
if (fHi > fLo) {
if (hi <= lo) { lo = fLo; hi = fHi; }
else if (fLo <= hi && lo <= fHi) { lo = std::min(lo, fLo); hi = std::max(hi, fHi); }
else { lo2 = fLo; hi2 = fHi; if (lo2 < lo) { std::swap(lo, lo2); std::swap(hi, hi2); } }
}
}
if (aBase && aBase->composed && aBase->invHi > aBase->invLo) {
// the rows the base's composite changed, in this frame: scrolled content dy rows up, fixed
// content where it was; the daemon cannot tell which, so the hull of both (round 116)
const long a = std::max(0L, std::min(aBase->invLo, aBase->invLo - dy)), c = std::min((long)H, std::max(aBase->invHi, aBase->invHi - dy));
if (c > a) {
if (hi <= lo) { lo = a; hi = c; }
else if (a <= hi && lo <= c) { lo = std::min(lo, a); hi = std::max(hi, c); }
else if (hi2 <= lo2) { lo2 = a; hi2 = c; }
else { lo2 = std::min(lo2, a); hi2 = std::max(hi2, c); } // the hull with the second band
if (hi2 > lo2 && lo2 <= hi && lo <= hi2) { lo = std::min(lo, lo2); hi = std::max(hi, hi2); lo2 = hi2 = 0; }
if (hi2 > lo2 && lo2 < lo) { std::swap(lo, lo2); std::swap(hi, hi2); }
}
}
}
unsigned char* pixels = S.base[slot] + S.hdr;
// the main lane's view for a compositor-side scroll: the card's zoom, the overscan's offset, the
// card's scroll in CSS px (gpuScrollTo, gpuSetView)
const double zr = (want.zoom >= 0.05 && want.zoom <= 20.0) ? want.zoom : 1.0;
const double view[5] = { Z, 0.0, -(double)os / Z, want.x / zr, want.y / zr };
const uint32_t token = mPage->ComposePost(pixels, S.seg[slot] - S.hdr, H, lo, hi, lo2, hi2, base ? dy : 0, src, srcY,
base, view);
if (!token) {
if (++mXFailures >= 3) fprintf(stderr, "[jihad-gpu] express lane off for this page: the compositor took no frame three times\n");
return false;
}
mXLastPostToken = token;
XJob& j = aBase ? mXJob2 : mXJob;
j = XJob();
j.on = true; j.token = token; j.gen = mXGeomGen; j.slot = slot; j.key = S.keys[slot];
j.x = x; j.y = y; j.w = w; j.h = H; j.os = os; j.zoom = Z; j.shift = base ? dy : 0;
j.t0 = now; j.wantMs = want.ms; j.waitMs = waitSince; j.whole = !base; j.pixels = pixels;
j.cardY = want.y;
return true;
}
bool BrowserPageGoanna::xPoll(int64_t now) {
if (!mPage) { mXJob.on = false; return false; }
const bool genOk = mXJob.gen == mXGeomGen && mPage->GpuPath();
GoannaRenderPage::ComposeResult cr;
const int r = mPage->ComposePoll(mXJob.token, genOk ? mXJob.pixels : nullptr, &cr);
if (r == -2 || r == -3) {
if (now - mXJob.t0 > 3000) {
xForget(mXJob2); xForget(mXJob); // the second builds on the first
if (++mXFailures >= 3) fprintf(stderr, "[jihad-gpu] express lane off for this page: three frames not composed in 3 s\n");
return false;
}
if (r == -3 && !mXJob.composed) {
mXJob.composed = true; mXJob.invLo = cr.invLo; mXJob.invHi = cr.invHi;
mXJob.composedY = cr.frameYValid ? (int)cr.frameY : mXJob.y;
}
return true;
}
if (!genOk || r < 0) {
// the buffers or the geometry changed under it, or it failed: dropped, and the one built on it
mXJob = XJob(); xForget(mXJob2);
if (genOk && ++mXFailures >= 3) fprintf(stderr, "[jihad-gpu] express lane off for this page: three frames failed\n");
return true;
}
mXFailures = 0;
const int s = mXJob.slot, H = mXJob.h, w = mXJob.w;
unsigned char* pixels = mXJob.pixels;
const int frameY = cr.frameYValid ? (int)cr.frameY : mXJob.y; // round 115: where the engine composed it
if (mXJob.shift) mPage->CleanRows(pixels, 0, H); // the shifted rows: the compositor thread's writes
// the frame's rows outside the page white, as the main lane does (T-505)
const long pageEnd = mXSnap.contentH > 0 ? (long)((double)mXSnap.contentH * mXJob.zoom) - frameY : (long)H;
const long above = std::min((long)H, std::max(0L, -(long)frameY));
if (above > 0) memset(pixels, 0xff, (size_t)above * w * 4);
if (pageEnd < H && pageEnd >= 0) memset(pixels + (size_t)pageEnd * w * 4, 0xff, (size_t)(H - pageEnd) * w * 4);
BrowserOffscreenInfo* oi = (BrowserOffscreenInfo*)(pixels - mXSnap.hdr);
oi->bufferWidth = w; oi->bufferHeight = H;
oi->contentZoom = mXJob.zoom;
oi->renderedX = mXJob.x; oi->renderedY = frameY;
oi->renderedWidth = w; oi->renderedHeight = H;
mSink.msgPainted(mXJob.key);
mInFlight[s] = true; mPaintMs[s] = now;
jihadTimelinePublish("express", mXJob.t0, frameY, mXJob.zoom);
if (!mXMainWindow && mActiveKey == mXJob.key) mActiveKey = keyAfter(mXJob.key);
// the main lane's records: this slot is the newest frame and the only one up to date (the
// composites of this lane are not in the other slots' stale rows)
for (int t = 0; t < 3; ++t) mGpuSlotValid[t] = false;
mGpuSlotValid[s] = true;
mGpuSlotX[s] = mXJob.x; mGpuSlotY[s] = frameY; mGpuSlotZoom[s] = mXJob.zoom; mGpuSlotW[s] = w; mGpuSlotH[s] = H;
mGpuStaleLo[s] = mGpuStaleHi[s] = mGpuStale2Lo[s] = mGpuStale2Hi[s] = 0;
mGpuLatestSlot = s; mGpuLatestKey = mXJob.key;
mGpuFixedLo = cr.fixedLo; mGpuFixedHi = cr.fixedHi;
mBufFull[s].valid = false;
mGpuJobX = mXJob.x; mGpuJobY = frameY; mGpuJobZoom = mXJob.zoom; mGpuJobViewRow = mXJob.os;
mPaintedX = mXJob.x; mPaintedW = w; mPaintedLo = frameY; mPaintedHi = (long)frameY + H; mPaintedZoom = mXJob.zoom;
mLastPaintDoneMs = now; gJihadPaintDone++; JihadRecordFrameGap(now);
mXPub.token = mXJob.token; mXPub.slot = s; mXPub.key = mXJob.key; mXPub.x = mXJob.x; mXPub.y = frameY;
mXPub.w = w; mXPub.h = H; mXPub.zoom = mXJob.zoom; mXPub.fixedLo = cr.fixedLo; mXPub.fixedHi = cr.fixedHi;
mXPub.cardX = mXJob.x; mXPub.cardY = mXJob.cardY; mXPub.ms = now;
mNeedsPaint = true; // the main lane's next frame: its engine half (the anchor, the zoom's release) and the newest move
++mXFrames;
mXLastZoom = mXJob.zoom;
mXLastXPubMs = now;
static const char* sZoomDump = getenv("JIHAD_GPU_ZOOM_DUMP");
if (sZoomDump) {
// T-504 device rounds: the express lane's frames at a new zoom in the 3 s after it, as the
// adapter reads them, to <path>.x<n> (n: the zoom's number as the reader thread counted it)
int64_t zMs; int zN; double zWant;
{ std::lock_guard<std::mutex> l(jihadWantLock()); zMs = mXZoomWantMs; zN = mXZoomDumpN; zWant = mXWant.zoom; }
const double zEff = (zWant >= 0.99 && zWant <= 1.01) ? 1.0 : zWant;
if (zN >= 1 && zN <= (strcmp(sZoomDump, "log") ? 6 : 60) && now - zMs < 3000 && fabs(mXJob.zoom - zEff) < 1e-6) {
char path[512];
snprintf(path, sizeof path, "%s.x%d", sZoomDump, zN);
const bool logOnly = strcmp(sZoomDump, "log") == 0; // the timeline only
FILE* f = logOnly ? nullptr : fopen(path, "wb");
if (f) { fwrite(pixels, 1, (size_t)w * H * 4, f); fclose(f); }
if (f || logOnly) {
fprintf(stderr, "[jihad-gpu] zoom frame written (express): %s (%dx%d at zoom %.4f, %ld ms after the zoom, x %d y %d)\n",
path, w, H, mXJob.zoom, (long)(now - zMs), mXJob.x, frameY); }
}
}
if (mXFrames <= 30 || (mXFrames % 50) == 0)
fprintf(stderr, "[jihad-gpu] express frame %u: slot %d %s (shift %ld, read %ld..%ld), zoom %.4f y %d, %ld ms after the card's"
" move, %ld ms after the card began to wait, composed and read in %ld ms, the main lane quiet %ld ms\n",
mXFrames, s, mXJob.whole ? "whole" : "from the newest", mXJob.shift, cr.readLo, cr.readHi, mXJob.zoom, frameY,
(long)(now - mXJob.wantMs), (long)(now - mXJob.waitMs), (long)(now - mXJob.t0),
mXMainPubMs ? (long)(now - mXMainPubMs) : -1L);
if (gJihadInputMarkMs && cr.invHi > cr.invLo) {
fprintf(stderr, "[jihad-gpu] input latency: the first changed frame (rows %ld..%ld, express) out %ld ms after the input\n",
cr.invLo, cr.invHi, (long)(now - gJihadInputMarkMs));
gJihadInputMarkMs = 0;
}
mXJob = mXJob2; // the second frame, if any, is the oldest now
mXJob2 = XJob();
return true;
}
bool BrowserPageGoanna::isBusy() const {
if (!mPage) return false;
// Engine media is playing somewhere in this process: its main-thread events (timeupdate,
@@ -3712,6 +4141,9 @@ void BrowserPageGoanna::paintToSharedBuffer() {
// A frame just refused as blank-over-good is retried on a grid, not every tick (see the
// blank-settle block below). mNeedsPaint stays set, so nothing is lost by waiting.
if (mBlankRetryAtMs && jihadNowMs() < mBlankRetryAtMs) { if (!mGpuJobs.empty()) mGpuTrace[2]++; return; }
// T-515: the express lane's frame is in flight (its composite is the newest, and the next frame
// builds on it): this one after it
if (mXJob.on) { mNeedsPaint = true; return; }
// Flow control (F-211): never overwrite a buffer the adapter still holds. If the target buffer
// was msgPainted but not yet returned, defer this paint (mNeedsPaint stays set, so the next
// pump tick / returnBuffer retries) — the typed text is already in the DOM, only the frame is
@@ -4315,6 +4747,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
mGpuViewRow = os;
mPage->SetGpuFrameHeight(gpuFrameH);
mPage->SetGpuOverscan(os, Zeff);
if (jihadExpressOn()) xSnapshot(w, h, gpuFrameH, os, hdr); // T-515: what the express lane composes into
}
paintedTop = (int)gpuLo; paintedH = gpuFrameH;
fresh = true; growOwed = false;
@@ -4331,8 +4764,44 @@ void BrowserPageGoanna::paintToSharedBuffer() {
const int gslot = slotForKey(mActiveKey);
if (!mGpuJobs.empty() && mGpuJobs[0].slot != gslot) gpuJobDrain(); // the slot moved under the jobs
if (mGpuZoomHoldMs && getenv("JIHAD_GPU_ZOOMLOG")) fprintf(stderr, "[jihad-gpu] zoom hold: GPU branch, slot %d, jobs %zu, zoom %.4f\n", gslot, mGpuJobs.size(), Zeff);
if (mGpuJobs.empty() && jihadExpressOn() && mXLastXPubMs && fabs(mXLastZoom - Zeff) > 1e-6 &&
jihadNowMs() - mXLastXPubMs < 300) {
// T-515: the express lane's newest frame is at a zoom this lane has not taken up yet (its
// thread came back from the page's script with the card's zoom still queued): a frame now
// would put the old zoom back on the card. The queued command comes first (300 ms at most).
mNeedsPaint = true;
return;
}
if (mGpuJobs.empty() && jihadExpressOn() && xLeads(jihadNowMs())) {
// T-515: the card is moving and the express lane makes its frames: this lane paints the
// layers (the displayport an anchor move asks for, the page's changes), at most every 40 ms,
// with the frame lock let go of, and posts no frame of its own
const int64_t now = jihadNowMs();
if (now - mXMainPaintMs >= 40) {
mXMainPaintMs = now;
mXMainSlot = gslot; mXMainWindow = true;
{ FrameLockRelease unlocked; mPage->PaintLayers(); jihadTimelineMain("layer paint (the express lane leads)", (long)(jihadNowMs() - now)); }
mXMainWindow = false; mXMainSlot = -1;
}
mNeedsPaint = true; // its own frame once the card stops
return;
}
if (mGpuJobs.empty()) {
if (!gpuJobStart(gslot, mActiveKey, pixels, segSize - hdr, (int)bandX, (int)gpuLo, Zeff, w, h)) {
if (jihadExpressOn() && mPage->ExpressReady()) {
// T-515: the layers painted with the frame lock let go of, so the express lane can make
// frames from the layer tree as it stands while this paint runs (a displayport move's
// paint is 60-120 ms on desktop Wikipedia). It keeps off this slot and the active key;
// the bands below are worked out after the paint, from the slots as they are then.
mXMainSlot = gslot; mXMainWindow = true;
bool painted;
{ const int64_t p0 = jihadNowMs(); FrameLockRelease unlocked; painted = mPage->PaintLayers(); jihadTimelineMain("layer paint", (long)(jihadNowMs() - p0)); }
mXMainWindow = false; mXMainSlot = -1;
if (mXJob.on) { mNeedsPaint = true; return; } // its frame went first: this one on the next tick
mGpuPainted = painted;
}
const bool started = gpuJobStart(gslot, mActiveKey, pixels, segSize - hdr, (int)bandX, (int)gpuLo, Zeff, w, h);
mGpuPainted = false;
if (!started) {
nb = -1;
} else {
mGpuJobPolls = 0;
@@ -4383,7 +4852,19 @@ void BrowserPageGoanna::paintToSharedBuffer() {
}
mGpuJobs.erase(mGpuJobs.begin());
nb = r;
if (gJihadInputMarkMs && r >= 0 && cr.invHi > cr.invLo) {
// T-515: the express lane published after this job was posted: its frames are newer and its
// composite the newest; this one is dropped (its slot's record with it)
const bool xDropped = r >= 0 && oldest.xFrames != mXFrames;
if (xDropped) {
if (gslot >= 0) mGpuSlotValid[gslot] = false;
static uint32_t sXD = 0;
if (sXD++ < 20 || (sXD % 50) == 0)
fprintf(stderr, "[jihad-gpu] main lane's frame dropped: the express lane published %u frame(s) since it was posted\n",
mXFrames - oldest.xFrames);
nb = -1;
mNeedsPaint = true;
}
if (gJihadInputMarkMs && r >= 0 && !xDropped && cr.invHi > cr.invLo) {
fprintf(stderr, "[jihad-gpu] input latency: the first changed frame (rows %ld..%ld) out %ld ms after the input\n",
cr.invLo, cr.invHi, (long)(jihadNowMs() - gJihadInputMarkMs));
gJihadInputMarkMs = 0;
@@ -4409,7 +4890,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
mGpuJobPolls = 0;
paintT0 = oldest.t0; // the painted line's ms= is then start-to-publish
}
if (r >= 0 && gslot >= 0) {
if (r >= 0 && gslot >= 0 && !xDropped) {
mGpuSlotValid[gslot] = true;
// round 115: the position the engine composed it at, when a scroll of the page's own moved it
const int frameY = cr.frameYValid ? (int)cr.frameY : oldest.y;
@@ -4417,6 +4898,23 @@ void BrowserPageGoanna::paintToSharedBuffer() {
mGpuSlotW[gslot] = oldest.w; mGpuSlotH[gslot] = oldest.h;
mGpuStaleLo[gslot] = mGpuStaleHi[gslot] = 0; // everything the slot lacked was read
mGpuLatestSlot = gslot; mGpuLatestKey = oldest.key; // round 113: the newest frame
if (jihadExpressOn()) {
// T-515: the express lane's base, if this is the newest composite; the card served
mXPub.token = oldest.token; mXPub.slot = gslot; mXPub.key = oldest.key; mXPub.x = oldest.x;
mXPub.y = cr.frameYValid ? (int)cr.frameY : oldest.y; mXPub.w = oldest.w; mXPub.h = oldest.h;
mXPub.zoom = oldest.zoom; mXPub.fixedLo = cr.fixedLo; mXPub.fixedHi = cr.fixedHi;
mXPub.cardX = oldest.x; mXPub.cardY = oldest.y + oldest.viewRow; mXPub.ms = jihadNowMs();
mXMainPubMs = mXPub.ms;
mXLastXPubMs = 0;
mXCmdPub = oldest.cmdDone;
{
// the card's wait ends with a frame that took up its newest command (a frame from before
// a zoom the main loop had not handled yet leaves the express lane to serve it)
std::lock_guard<std::mutex> l(jihadWantLock());
if (!mXWant.seq || mXCmdPub >= mXWant.cmdSeq) mXWaitSinceMs = 0;
mXZoomNow = mZoom;
}
}
mGpuStale2Lo[gslot] = mGpuStale2Hi[gslot] = 0;
mGpuFixedLo = cr.fixedLo; mGpuFixedHi = cr.fixedHi;
// The other slots: the rows this frame's tree change touched are stale in them too. The
@@ -4468,6 +4966,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
mGpuViewRow = oldest.viewRow; gpuSetView(vz, mGpuViewRX, mGpuViewRY);
mPage->SetGpuScroll(oldest.x / vz, (oldest.y + oldest.viewRow) / vz, true);
const uint32_t vt = mPage->ComposeStart(sScratch.data(), bytes, 0, h, 0, 0, 0, nullptr, 0, false, true); // T-503: a verify composite
if (vt) mXLastPostToken = vt; // T-515: the newest composite is not a published frame
int r2 = -1;
if (vt) {
GoannaRenderPage::ComposeResult vr;
@@ -4574,6 +5073,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
mGpuViewRow = oldest.viewRow; gpuSetView(vz, mGpuViewRX, mGpuViewRY);
mPage->SetGpuScroll(oldest.x / vz, (oldest.y + oldest.viewRow - oldest.shift) / vz, true);
const uint32_t vt2 = mPage->ComposeStart(sBefore.data(), bytes, 0, h, 0, 0, 0, nullptr, 0, false, true);
if (vt2) mXLastPostToken = vt2; // T-515
int r3 = -1;
if (vt2) {
GoannaRenderPage::ComposeResult vr2;
@@ -4629,6 +5129,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
int popups = mPage->CompositePopups(pixels, paintedW * 4, paintedW, paintedH,
(double)px / pz, (double)py / pz, pz);
popupsDrawn = popups;
mXSnap.popups = popups > 0; // T-515: the express lane draws no popups: none while one is open
if (popups > 0) {
fprintf(stderr, "[jihad-bs] composited %d popup(s) over the frame\n", popups);
// A popup is content: an otherwise-blank frame carrying one must not be
@@ -4662,7 +5163,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
gJihadPaintDone++;
JihadRecordFrameGap(mLastPaintDoneMs);
static const char* sZoomDump = getenv("JIHAD_GPU_ZOOM_DUMP");
if (sZoomDump && nb > 0 && mPage->GpuPath() && mGpuZoomDumpN >= 1 && mGpuZoomDumpN <= 6 &&
if (sZoomDump && nb > 0 && mPage->GpuPath() && mGpuZoomDumpN >= 1 && mGpuZoomDumpN <= (strcmp(sZoomDump, "log") ? 6 : 60) &&
jihadNowMs() - mGpuZoomCmdMs < 3000 && fabs(oi->contentZoom - mGpuZoom) < 1e-6) {
// T-504 device rounds (JIHAD_GPU_ZOOM_DUMP=<path>): every frame at the new zoom published in the
// 3 s after a zoom, as the adapter will read it, rewritten to <path>.<n> (n: the zoom's number,
@@ -4670,10 +5171,20 @@ void BrowserPageGoanna::paintToSharedBuffer() {
// (cavekit-gpu-rendering R6: the card covered within 500 ms of the gesture)
char path[512];
snprintf(path, sizeof path, "%s.%d", sZoomDump, mGpuZoomDumpN);
FILE* f = fopen(path, "wb");
if (f) { fwrite(pixels, 1, (size_t)paintedW * paintedH * 4, f); fclose(f);
fprintf(stderr, "[jihad-gpu] zoom frame written: %s (%dx%d at zoom %.4f, %ld ms after the zoom, x %d y %d)\n",
path, paintedW, paintedH, oi->contentZoom, (long)(jihadNowMs() - mGpuZoomCmdMs), oi->renderedX, oi->renderedY); }
// T-515: with the express lane, also from when the card's zoom ARRIVED (the reader thread): the
// main loop may have taken it up only after the page's script let go
char arrived[64] = "";
if (jihadExpressOn()) {
int64_t zMs; { std::lock_guard<std::mutex> l(jihadWantLock()); zMs = mXZoomWantMs; }
if (zMs) snprintf(arrived, sizeof arrived, ", %ld ms after it arrived", (long)(jihadNowMs() - zMs));
}
// JIHAD_GPU_ZOOM_DUMP=log: the timeline only (a frame is 3-4 MB of the device's 40 MB /tmp)
const bool logOnly = strcmp(sZoomDump, "log") == 0;
FILE* f = logOnly ? nullptr : fopen(path, "wb");
if (f) { fwrite(pixels, 1, (size_t)paintedW * paintedH * 4, f); fclose(f); }
if (f || logOnly) {
fprintf(stderr, "[jihad-gpu] zoom frame written: %s (%dx%d at zoom %.4f, %ld ms after the zoom%s, x %d y %d)\n",
path, paintedW, paintedH, oi->contentZoom, (long)(jihadNowMs() - mGpuZoomCmdMs), arrived, oi->renderedX, oi->renderedY); }
}
if (nb >= 0 && mPage->GpuPath()) {
// T-502 device rounds: the buffer as the adapter will read it (JIHAD_GPU_DUMP2=<path>, the 5th frame)
@@ -4919,6 +5430,7 @@ void BrowserPageGoanna::paintToSharedBuffer() {
const int64_t a = jihadNowMs();
mSink.msgPainted(mActiveKey);
const int64_t b = jihadNowMs();
jihadTimelinePublish("main", gpuFinished ? paintT0 : 0, gpuFinished ? mGpuJobY : paintedTop, oi->contentZoom);
static int sN = 0;
if (mGpuDoneMs && sN++ < 40)
fprintf(stderr, "[jihad-gpu] publish: %ld ms from the frame's arrival to the message, msgPainted %ld ms\n",
+66
View File
@@ -192,6 +192,20 @@ public:
// to the card that STARTED it, not to whichever card connected last.
const void* docShellKey() const { return mPage ? mPage->DocShellKey() : nullptr; }
void returnBuffer(int sharedBufferKey); // YAP: returnBuffer (adapter freed it)
// T-515, the express lane (JihadFrameLock.h, BrowserPageGoanna.cpp): the card's scroll and zoom
// (the want lock held by the caller) and its returned buffers (the frame lock held) as the YAP
// reader thread sees them, ahead of the main loop, and the express thread's step (the frame lock
// held; true: step again soon).
// tag: the command's number in its proxy's arrival order (YapProxy); the main loop reports the
// tag of each scroll or zoom it handled (the frame lock): the main lane's frame serves the card
// once it has taken up the newest one
void expressWant(int x, int y, double zoom, bool hasZoom, uint32_t tag);
void expressCommandDone(uint32_t tag) { if (tag > mXCmdDone) mXCmdDone = tag; }
void expressReturn(int sharedBufferKey);
bool expressStep(int64_t now);
// out of the express lane's reach at once, its frames waited out: the card left (reap), its proxy
// is deleted now and this page perhaps only at the tick's end
void expressDetach();
void freeze(); // YAP: freeze (card backgrounded — pause paint)
void thaw(int key1, int key2, int size); // YAP: thaw (reattach buffers, resume)
// Re-thaw onto the buffers we already hold. The inject channel called thaw(0,0,0), and
@@ -397,6 +411,8 @@ private:
uint32_t token = 0; int slot = -1, key = 0, x = 0, y = 0, w = 0, h = 0; double zoom = 1.0;
int64_t t0 = 0; uint32_t seq = 0; long askLo = 0, askHi = 0, ask2Lo = 0, ask2Hi = 0, shift = 0;
int viewRow = 0; // T-505: the view's top in the frame (the overscan above it)
uint32_t xFrames = 0; // T-515: the express lane's frames when it was posted (newer ones since: dropped)
uint32_t cmdDone = 0; // T-515: the card's commands the main loop had handled when it was posted
};
// T-505: the view's row in the GPU frame and the residual offset last asked of the compositor
int mGpuViewRow = 0;
@@ -436,7 +452,57 @@ private:
gpuJobDrain();
for (int i = 0; i < 3; ++i) mGpuSlotValid[i] = false;
mGpuLatestSlot = -1; // round 113
xInvalidate();
}
// T-515, the express lane's state. All of it under the frame lock (JihadFrameLock.h).
struct XWant { int x = 0, y = 0; double zoom = 1.0; int64_t ms = 0; uint32_t seq = 0, cmdSeq = 0; };
uint32_t mXCmdDone = 0; // the newest of the card's commands the main loop handled (its tag; the frame lock)
uint32_t mXCmdPub = 0; // ...the main lane's newest frame took up
XWant mXWant; // the card's newest position (the reader thread's view; the want lock)
int64_t mXWaitSinceMs = 0; // the card has waited for a frame since (the main lane's publish clears it; the want lock)
double mXZoomNow = 1.0; // the zoom the main lane last applied (for a scroll before any zoom; the want lock)
int64_t mXZoomWantMs = 0; // the card's last new zoom arrived (the want lock; JIHAD_GPU_ZOOM_DUMP)
int mXZoomDumpN = 0; // ...its number (the want lock)
double mXLastZoom = 0.0; // the zoom of the express lane's last frame (the zoom hold's release)
int64_t mXLastXPubMs = 0; // the express lane's last publish, while it is the newest frame (0: the main lane's is)
int mXConsumed[3] = {0, 0, 0}; // returns the express hook applied; returnBuffer skips as many
// the main lane's GPU frame geometry and buffers, as of its last frame
struct XSnap {
bool ok = false; int w = 0, h = 0, frameH = 0, os = 0; size_t hdr = 0;
int keys[3] = {0, 0, 0}; size_t seg[3] = {0, 0, 0}; unsigned char* base[3] = {nullptr, nullptr, nullptr};
long contentH = 0; bool popups = false;
};
XSnap mXSnap;
uint32_t mXGeomGen = 0; // the buffers, the frame's geometry or the document changed
uint32_t mXLastPostToken = 0; // the newest composite asked for, by either lane
// the newest frame composed intact into a slot (either lane): the express lane's base
struct XPub {
uint32_t token = 0; int slot = -1, key = 0, x = 0, y = 0, w = 0, h = 0; double zoom = 0.0;
long fixedLo = 0, fixedHi = -1; int cardX = 0, cardY = 0; int64_t ms = 0;
};
XPub mXPub;
int64_t mXMainPubMs = 0; // the main lane's last publish
struct XJob {
bool on = false; uint32_t token = 0, gen = 0; int slot = -1, key = 0, x = 0, y = 0, w = 0, h = 0, os = 0;
double zoom = 1.0; long shift = 0; int64_t t0 = 0, wantMs = 0, waitMs = 0; bool whole = true;
unsigned char* pixels = nullptr; int cardY = 0;
bool composed = false; long invLo = 0, invHi = 0; int composedY = 0; // known once composed (being read)
};
XJob mXJob; // the express lane's frame in flight (the older one)
XJob mXJob2; // the one after it, built on it while it is read (round 130's rule)
bool mXMainWindow = false; // the main lane paints its layers with the lock let go of
int mXMainSlot = -1; // ...for this slot
uint32_t mXFrames = 0, mXFailures = 0;
bool mGpuPainted = false; // gpuJobStart: the layers are painted already (post only)
// (and the main lane's first frame of the new geometry or document comes first: its blank
// suppression and first-paint hold are the main lane's)
void xInvalidate() { ++mXGeomGen; mXSnap.ok = false; mXPub = XPub(); mXMainPubMs = 0; }
bool xStart(int64_t now, double zoom, const XWant& want, int64_t waitSince, const XJob* aBase = nullptr);
void xForget(XJob& j) { if (j.on && mPage) mPage->ComposeForget(j.token); j = XJob(); }
bool xLeads(int64_t now); // the card is moving: the express lane makes the frames (the frame lock held)
int64_t mXMainPaintMs = 0; // the main lane's last layer paint while the express lane leads
bool xPoll(int64_t now);
void xSnapshot(int w, int h, int frameH, int os, size_t hdr);
// window opens (inspector P2: cap dirty-driven full repaints)
int64_t mLastDirtyPaintMs; // last dirty-driven repaint (ms), for the 150 ms cap
// Next scheduled publish instant for plugin-driven damage. A GRID, deliberately not
+14
View File
@@ -64,6 +64,17 @@ extern "C" {
int jihad_offscreen_compose_poll(nsIWidget* aWidget, uint32_t aToken, int* aOut,
int* aScrollOnly) __attribute__((weak));
void jihad_offscreen_compose_forget(nsIWidget* aWidget, uint32_t aToken) __attribute__((weak));
// OPTIONAL (T-515): the start's two halves on their own. paint_layers (the main thread) flushes
// and paints the layers; compose_post (any thread) posts the compose + read of the layer tree as
// it stands, at the view given (aUseView 1: scale, offset, scroll) or the one last set (0).
int jihad_offscreen_paint_layers(nsIWidget* aWidget, nsIDocShell* aDocShell) __attribute__((weak));
// peek: a job's state without taking its result (1 done, 2 composed and being read, 0 pending, -1 unknown)
int jihad_offscreen_compose_peek(nsIWidget* aWidget, uint32_t aToken) __attribute__((weak));
uint32_t jihad_offscreen_compose_post(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) __attribute__((weak));
// OPTIONAL (T-502): the card's zoom and the visual viewport's residual offset (CSS px, what the
// engine scroll could not reach), applied by the compositor to the whole layer tree.
void jihad_offscreen_set_view(nsIWidget* aWidget, double aScale, double aOffsetX,
@@ -129,6 +140,9 @@ inline void ReportEngineExports() {
{"jihad_offscreen_compose_start", jihad_offscreen_compose_start != nullptr},
{"jihad_offscreen_compose_poll", jihad_offscreen_compose_poll != nullptr},
{"jihad_offscreen_compose_forget", jihad_offscreen_compose_forget != nullptr},
{"jihad_offscreen_paint_layers", jihad_offscreen_paint_layers != nullptr},
{"jihad_offscreen_compose_post", jihad_offscreen_compose_post != nullptr},
{"jihad_offscreen_compose_peek", jihad_offscreen_compose_peek != nullptr},
{"jihad_offscreen_compositor_seq", jihad_offscreen_compositor_seq != nullptr},
{"jihad_offscreen_set_view", jihad_offscreen_set_view != nullptr},
{"jihad_offscreen_set_scroll", jihad_offscreen_set_scroll != nullptr},
+27
View File
@@ -4709,6 +4709,33 @@ static void GpuCleanRows(unsigned char* dst, int w, int h, long lo, long hi) {
void GoannaRenderPage::CleanRows(unsigned char* dst, long lo, long hi) { GpuCleanRows(dst, mWidth, GpuFrameHeight(), lo, hi); }
// T-515: the frame's two halves on their own (the express lane: frames while the main thread is busy)
bool GoannaRenderPage::ExpressReady() const {
return ComposeAsync() && jihad_offscreen_paint_layers && jihad_offscreen_compose_post && jihad_offscreen_compose_peek;
}
int GoannaRenderPage::ComposePeek(uint32_t token) {
return (mWidget && jihad_offscreen_compose_peek) ? jihad_offscreen_compose_peek(mWidget, token) : -1;
}
bool GoannaRenderPage::PaintLayers() {
if (!ExpressReady() || !mWidget || !mChrome || !mChrome->mBrowser) return false;
nsCOMPtr<nsIDocShell> ds = GetDocShell(mChrome->mBrowser);
return ds && jihad_offscreen_paint_layers(mWidget, ds) != 0;
}
uint32_t GoannaRenderPage::ComposePost(unsigned char* dst, size_t dstBytes, int frameH, long rowLo, long rowHi, long row2Lo,
long row2Hi, long shift, const unsigned char* src, long srcY, bool engineShift,
const double* aView) {
if (!ExpressReady() || !mWidget || !dst) return 0;
const int w = mWidth;
if (w <= 0 || frameH <= 0 || dstBytes < (size_t)w * frameH * 4) return 0;
return jihad_offscreen_compose_post(mWidget, dst, w * 4, w, frameH, (int)rowLo, (int)rowHi, (int)row2Lo, (int)row2Hi,
(int)shift, src, (int)srcY, engineShift ? 1 : 0, aView ? 1 : 0,
aView ? aView[0] : 1.0, aView ? aView[1] : 0.0, aView ? aView[2] : 0.0,
aView ? aView[3] : 0.0, aView ? aView[4] : 0.0, aView ? 1 : 0);
}
// T-509: the resolution back to 1 when the GPU path is given up
void GoannaRenderPage::ResetGpuResolution() {
if (!mChrome || !mChrome->mBrowser) return;
+9
View File
@@ -373,6 +373,15 @@ public:
uint32_t ComposeStart(unsigned char* dst, size_t dstBytes, long rowLo, long rowHi, long row2Lo = 0, long row2Hi = 0, long shift = 0,
const unsigned char* src = nullptr, long srcY = 0, bool engineShift = false, bool verify = false);
void CleanRows(unsigned char* dst, long lo, long hi); // the cache clean of rows another thread wrote
// T-515: ComposeStart's halves. PaintLayers (the main thread) paints the layers; ComposePost (any
// thread, the frame lock held) composes the tree as it stands into a frameH-row frame, at aView
// (scale, offset x, offset y, scroll x, scroll y: CSS px, the scroll valid) or, null, the view and
// scroll last set. ExpressReady: both exports are in the engine.
bool PaintLayers();
uint32_t ComposePost(unsigned char* dst, size_t dstBytes, int frameH, long rowLo, long rowHi, long row2Lo, long row2Hi,
long shift, const unsigned char* src, long srcY, bool engineShift, const double* aView);
bool ExpressReady() const;
int ComposePeek(uint32_t token); // 1 done, 2 composed (being read), 0 pending, -1 unknown
// askLo/askHi: the band that job was started with (what was read is it joined with the change)
// out (may be null): the rows the tree changed (inv), the rows fixed layers cover (fixed),
// the two bands read (read, read2), and whether the composite's only change was the scroll
+145
View File
@@ -0,0 +1,145 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef JIHAD_FRAME_LOCK_H
#define JIHAD_FRAME_LOCK_H
// T-515, frames made off the main thread: the daemon's frame lock.
//
// WHY. The daemon made every frame on its main thread, the engine's, so a page's script that ran
// for a second (desktop Wikipedia restyling after a scroll, a zoom's resize handlers) held every
// frame for that second: the card's pan and zoom waited with it (cavekit-gpu-rendering R3, R6).
// The compositor composes the layer tree on its own thread already; what kept the frame on the
// main thread was the daemon's own bookkeeping (the buffers, the slots, the publish). With the
// express lane (BrowserPageGoanna.cpp) a second thread composes and publishes frames from the
// layer tree as it stands while the main thread is busy in the engine.
//
// THE LOCK. One per daemon, around every part of that bookkeeping. The main thread holds it for
// the whole of its tick and of every card command, and lets go of it only where it runs the
// engine for long: the pump (the page's script, timers, network events) and the layer paint of a
// frame. The express lane, the YAP reader threads (the card's scroll, zoom and returned buffers)
// and the inject pan take it for their short steps. Re-entrant on the thread that holds it, so a
// function that needs it takes it whether or not its caller already did; release() lets go of all
// the holds at once and reacquire() takes them back, so a release deep in a call chain works.
//
// THE WANT LOCK. The YAP reader thread never waits for the frame lock: a proxy's teardown joins
// its reader, and the main thread can tear one down holding the frame lock (a card that leaves
// while a command's nested loop runs). The card's newest scroll and zoom (and the page that a
// proxy's commands go to) are under a lock of their own, held only for a few assignments; a
// returned buffer takes the frame lock with a bound (tryLockFor) and is left to the main loop
// when it is busy. Order: the frame lock, then the want lock.
//
// Off (every call a no-op) unless the express lane is on (jihadExpressOn): with it off the daemon
// is single-threaded as it always was.
#include <atomic>
#include <cstdint>
#include <mutex>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
namespace jihad {
bool jihadExpressOn(); // BrowserPageGoanna.cpp: JIHAD_GPU_EXPRESS, the GPU path only
void jihadExpressStop(); // BrowserPageGoanna.cpp: the express thread ended (the daemon's exit; the frame lock NOT held)
class FrameLock {
public:
void lock() {
if (owned()) { ++mDepth; return; }
mMutex.lock();
mOwner.store(pthread_self()); mHeld.store(true);
mDepth = 1;
}
void unlock() {
if (--mDepth == 0) { mHeld.store(false); mMutex.unlock(); }
}
// lock() with a bound: false (not held) when another thread kept it for aMs
bool tryLockFor(int aMs) {
if (owned()) { ++mDepth; return true; }
const int64_t until = nowMs() + aMs;
for (;;) {
if (mMutex.try_lock()) {
mOwner.store(pthread_self()); mHeld.store(true);
mDepth = 1;
return true;
}
if (nowMs() >= until) return false;
usleep(300);
}
}
// all of this thread's holds let go of; the count to give reacquire(). aAway: the main thread
// goes into the engine (the pump, a layer paint) and is away from its frames until it is back.
int release(bool aAway = false) {
if (!owned()) return 0;
const int d = mDepth;
if (aAway && !mAwayMs.load()) mAwayMs.store(nowMs());
mDepth = 0; mHeld.store(false);
mMutex.unlock();
return d;
}
// aAwayBefore: the away mark as release() found it (a release nested in an outer one's engine
// call, a layer paint from a command the pump's nested loop ran, leaves the outer one's mark)
void reacquire(int depth, int64_t aAwayBefore = 0) {
if (depth <= 0) return;
mMutex.lock();
mOwner.store(pthread_self()); mHeld.store(true);
mDepth = depth;
mAwayMs.store(aAwayBefore);
}
// since when the main thread has been in the engine with the lock let go of (0: it is not)
int64_t awaySinceMs() const { return mAwayMs.load(); }
static int64_t nowMs() {
struct timeval tv; gettimeofday(&tv, nullptr);
return (int64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
bool owned() const { return mHeld.load() && pthread_equal(mOwner.load(), pthread_self()); }
private:
std::mutex mMutex;
std::atomic<pthread_t> mOwner{};
std::atomic<bool> mHeld{false};
std::atomic<int64_t> mAwayMs{0};
int mDepth = 0; // the owner's holds; only the owner reads or writes it
};
inline FrameLock& jihadFrameLock() {
static FrameLock sLock;
return sLock;
}
inline std::mutex& jihadWantLock() {
static std::mutex sLock;
return sLock;
}
// The lock for a scope, when the express lane is on.
class FrameLockGuard {
public:
FrameLockGuard() : mOn(jihadExpressOn()) { if (mOn) jihadFrameLock().lock(); }
~FrameLockGuard() { if (mOn) jihadFrameLock().unlock(); }
FrameLockGuard(const FrameLockGuard&) = delete;
FrameLockGuard& operator=(const FrameLockGuard&) = delete;
private:
bool mOn;
};
// The lock let go of for a scope (the main thread's pump or layer paint: it is away from its
// frames until the scope ends) and taken back after.
class FrameLockRelease {
public:
FrameLockRelease()
: mAwayBefore(jihadExpressOn() ? jihadFrameLock().awaySinceMs() : 0),
mDepth(jihadExpressOn() ? jihadFrameLock().release(true) : 0) {}
~FrameLockRelease() { jihadFrameLock().reacquire(mDepth, mAwayBefore); }
FrameLockRelease(const FrameLockRelease&) = delete;
FrameLockRelease& operator=(const FrameLockRelease&) = delete;
private:
int64_t mAwayBefore;
int mDepth;
};
} // namespace jihad
#endif // JIHAD_FRAME_LOCK_H
+38
View File
@@ -0,0 +1,38 @@
<!doctype html>
<!-- Copyright 2026 NotAlexNoyle. MPL-2.0.
jihad-script-hold.html — a long page whose script holds the engine's main thread most of the
time (600 ms of busy loop every 700 ms), for T-515 (cavekit-gpu-rendering R3, R6): the card's
pan and zoom must still get frames while the script runs, from the layer tree as it stands.
?hold=<ms>&every=<ms> change the loop; ?hold=0 turns it off (the same page as a control). -->
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>jihad-script-hold</title>
<style>
body { margin: 0; font: 20px/1.4 sans-serif; background: #f4f1ea; }
#header { position: fixed; top: 0; left: 0; right: 0; height: 56px; background: #6b2b2b; color: #fff;
line-height: 56px; padding-left: 16px; z-index: 3; }
#content { padding: 72px 16px 16px; }
.band { height: 180px; margin: 12px 0; border-radius: 6px; color: #fff; font-size: 28px; padding: 12px; }
p { margin: 10px 0; }
</style></head>
<body>
<div id="header">jihad-script-hold <span id="state"></span></div>
<div id="content"></div>
<script>
var q = {}; location.search.replace(/[?&]([^=]+)=([^&]*)/g, function (m, k, v) { q[k] = +v; });
var hold = q.hold !== undefined ? q.hold : 600, every = q.every || 700;
var c = document.getElementById('content'), html = '';
var colors = ['#2b6c4e', '#2b4c7e', '#7e5a2b', '#5a2b7e', '#7e2b4c', '#2b7e7a'];
for (var i = 0; i < 60; i++) {
html += '<div class="band" style="background:' + colors[i % colors.length] + '">Band ' + i + '</div>';
html += '<p>Paragraph ' + i + ': the quick brown fox jumps over the lazy dog, and the card keeps its frames ' +
'while the page is busy. Pack my box with five dozen liquor jugs.</p>';
}
c.innerHTML = html;
var n = 0;
if (hold > 0) setInterval(function () {
var t = Date.now();
while (Date.now() - t < hold) {}
document.getElementById('state').textContent = '(held ' + (++n) + ')';
}, every);
</script>
</body></html>
+1 -1
+20
View File
@@ -125,3 +125,23 @@ the GPU path up for the daemon's life); the engine's test switches `JIHAD_GPU_TE
`JIHAD_GPU_TEST_STALL=<n>`; `three-daemons.sh gpu|soft` starts the three variants' daemons by hand
on one path, opens a page in each and reports memory, idle CPU and LunaSysMgr's state;
`three-daemons-restore.sh` puts the upstart jobs back.
T-515, rounds 240-267 (the express lane: frames made off the main thread): `JIHAD_GPU_EXPRESS=1` with
the GPU path (each card's YAP commands read on a thread of the proxy's own, the frame lock, an
express thread that composes and publishes frames while the main thread is in the page's script or
a layer paint; `render/goanna/JihadFrameLock.h` says how), `JIHAD_GPU_EXPRESS_MS=<ms>` (45: how long
the card waits before the express lane fills a stall), `JIHAD_GPU_EXPRESS_AWAY_MS=<ms>` (15: how
long the main thread has been in the engine first), `JIHAD_GPU_EXPRESS_LEAD_MS=<ms>` (120: while the
card's last scroll or zoom is that recent the express lane makes every frame and the main lane paints
layers only; 0 turns the lead off), `JIHAD_GPU_EXPRESS_EARLY=1` (the second frame posted right behind
the first: no faster on Wikipedia, older frames), `JIHAD_GPU_TIMELINE=1` (`[jihad-tl] t=<ms>` per
published frame of either lane and per long pump or layer paint, one clock),
`JIHAD_GPU_ZOOM_DUMP=log` (the zoom timeline without the 3-4 MB frame files; with the express lane
each main frame also says how long after the zoom ARRIVED). With the express lane the inject
channel's `pan` and `zoom <z> <x> <y> [delay ms]` go through the card's own path (the proxy's reader
side), on a thread, so they arrive while the main thread is busy as a card's do.
`zoom-seq.sh <name> <url> "<env>" [n]` sends n zooms that way at five points of a page script's
hold and prints each first frame from the zoom's arrival; `render/goanna/test/jihad-script-hold.html`
holds the main thread 600 ms in every 700 (`?hold=<ms>&every=<ms>`; `?hold=0` off). Express off
(`JIHAD_GPU_EXPRESS_MS=999999 JIHAD_GPU_EXPRESS_LEAD_MS=0` with it on) is the same timing path
without its frames: the baseline.
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# zoom-seq.sh <name> <url> "<env words>" [count]: T-515 (cavekit-gpu-rendering R6). The spike start
# with the inject channel and JIHAD_GPU_ZOOM_DUMP=log (the zoom timeline without frame files), then
# <count> zooms (default 8) through the card's path, in and out in turn, 2 s apart, each sent
# 0, 150, 300, 450 or 600 ms after the daemon reads it (the inject file is read only when the main
# thread is free: the delay lands the zoom at every point of a page script's hold, as a card's
# pinch does). Then, per zoom, the first frame at it from the zoom's arrival, whichever lane made
# it (the express lane's "N ms after the zoom", the main lane's "N ms after it arrived"). Needs
# JIHAD_GPU_EXPRESS=1 in the env words (the card's path is the express lane's reader thread).
set -u
S=$(dirname "$0"); R=/home/notalexnoyle/eclipse-workspace/Jihad/Jihad-Browser; D=$R/docs/rounds/t502-2026-10-17
name=$1; url=$2; extra=${3:-}; count=${4:-8}
cd $R; . build/webos-oe/host-path.sh
echo "=== zoom sequence $name"
build/webos-oe/dev-sh.sh -t 90 $S/gpu-spike-start.sh "$url" "JIHAD_INJECT=1 JIHAD_GPU_ZOOM_DUMP=log $extra" 2>&1 | grep 'screenshot at\|FATAL' | tail -n 2
{
echo 'STATE=/var/palm/jihad/enyo'
echo "sleep ${PAN_DELAY:-0}"
echo 'umask 077'
for i in $(seq 0 $((count - 1))); do
d=$(( (i % 5) * 150 ))
if [ $((i % 2)) -eq 0 ]; then z="zoom 1.6918 504 590 $d"; else z="zoom 0.6882 0 0 $d"; fi
echo "echo '$z' > \$STATE/inject.cmd; sleep 2"
done
echo 'sleep 2'
} > $S/zoom-seq-inject.sh
build/webos-oe/dev-sh.sh -t $((60 + ${PAN_DELAY:-0} + count * 2)) $S/zoom-seq-inject.sh
build/webos-oe/dev-sh.sh -t 40 $S/gpu-spike-collect.sh 2>/dev/null | sed -n '/=== LOG BEGIN/,/=== LOG END/p' | grep -v 'Fontconfig warning' > $D/zoomseq-$name-daemon-gpu.log
python3 - "$D/zoomseq-$name-daemon-gpu.log" <<'PYEOF'
import re, sys
lines = open(sys.argv[1], encoding="latin1").read().splitlines()
res = []
for i, l in enumerate(lines):
m = re.search(r"inject zoom ([0-9.]+) at .* in (\d+) ms", l)
if not m:
continue
z, d = m.group(1), m.group(2)
got = None
for l2 in lines[i + 1:]:
if "inject zoom" in l2:
break
if "zoom frame written" in l2 and ("at zoom " + z) in l2:
mx = re.search(r"\(express\).* (\d+) ms after the zoom", l2)
mm = re.search(r"(\d+) ms after it arrived", l2)
if mx: got = ("express", int(mx.group(1)))
elif mm: got = ("main", int(mm.group(1)))
if got: break
res.append((z, d, got))
for z, d, got in res:
print("zoom %s sent %s ms in: %s" % (z, d, "%s lane, %d ms after it arrived" % got if got else "no frame at it logged"))
ms = [g[1] for _, _, g in res if g]
if ms:
print("first frames: %d of %d, max %d ms, median %d ms" % (len(ms), len(res), max(ms), sorted(ms)[len(ms) // 2]))
PYEOF