feat(cli): check names a WebGL scene that draws while it is not on screen

This commit is contained in:
Miguel Angel Simon Sierra
2026-11-03 18:33:23 -05:00
parent e2edc93f5f
commit 5c5a1496ea
4 changed files with 94 additions and 0 deletions
@@ -2206,6 +2206,55 @@
return issues;
}
// Canvases whose WebGL context drew since the last audit; the author's own hf-seek loop decides what draws.
const webglDraws = (window.__hyperframesWebglDraws ??= new Set());
const drawGate = Symbol.for("hyperframes.webglDrawGate");
for (const name of ["WebGLRenderingContext", "WebGL2RenderingContext"]) {
const proto = window[name]?.prototype;
if (!proto || proto.__hyperframesDrawCount) continue;
proto.__hyperframesDrawCount = true;
for (const method of [
"clear",
"drawArrays",
"drawElements",
"drawArraysInstanced",
"drawElementsInstanced",
]) {
const draw = proto[method];
if (typeof draw !== "function") continue;
proto[method] = function (...args) {
if (!window[drawGate]?.skip) webglDraws.add(this.canvas);
return draw.apply(this, args);
};
}
}
function inactiveWebglIssues(root, time) {
const drawn = [...webglDraws].filter((canvas) => canvas?.isConnected);
webglDraws.clear();
return drawn
.filter(
(canvas) => canvas.closest(RUNTIME_HIDDEN_CLIP) || canvas.checkVisibility?.() === false,
)
.map((canvas) => {
const scene =
canvas.closest("[data-composition-id]:not([data-width])") ??
canvas.closest("[data-composition-id]");
const name = scene?.getAttribute("data-composition-id") || uniqueSelectorFor(canvas);
return {
code: "webgl_draws_while_inactive",
severity: "warning",
time,
selector: uniqueSelectorFor(canvas),
containerSelector: uniqueSelectorFor(root),
message: `Scene "${name}" draws WebGL while it is not on screen, so every seek pays for it.`,
rect: toRect(canvas.getBoundingClientRect()),
fixHint:
"Draw only while the scene is active (its data-start to data-start + data-duration), or skip the draw in your hf-seek listener when the scene is hidden.",
};
});
}
// Soft prior only — the counterfactual attach test (below) is what makes detachment a finding.
const CONNECTOR_NAME = /\b(conn(ector)?|arrow|edge|link|flow|wire)\b/i;
const CONNECTOR_SKIP_CONTAINERS = "defs, marker, clipPath, mask, symbol, pattern";
@@ -2631,6 +2680,7 @@
issues.push(...escaped.issues);
issues.push(...panelOutOfCanvasIssues(root, rootRect, time, tolerance, escaped.flagged));
issues.push(...canvasEdgeIssues(root, rootRect, time, tolerance));
issues.push(...inactiveWebglIssues(root, time));
issues.push(...connectorDetachmentIssues(root, rootRect, time));
issues.push(...connectorOrphanIssues(root, rootRect, time));
return issues;
@@ -1310,6 +1310,48 @@ describe("layout-audit.browser coordinate-frame findings", () => {
expect(issues.find((issue) => issue.selector === "#bleed")).toMatchObject({ severity: "info" });
});
describe("webgl_draws_while_inactive", () => {
function mountScenes(hidden: boolean): HTMLCanvasElement {
document.body.innerHTML = `
<div id="root" data-composition-id="main" data-width="640" data-height="360">
<div id="scene2" data-composition-id="scene2" data-start="4" data-duration="2" ${hidden ? "data-hf-clip-hidden" : ""}>
<canvas id="shader" width="640" height="360"></canvas>
</div>
</div>
`;
installGeometry({
root: rect({ left: 0, top: 0, width: 640, height: 360 }),
scene2: rect({ left: 0, top: 0, width: 640, height: 360 }),
shader: rect({ left: 0, top: 0, width: 640, height: 360 }),
});
installAuditScript();
return document.getElementById("shader") as HTMLCanvasElement;
}
const drawn = () =>
(window as unknown as { __hyperframesWebglDraws: Set<Element> }).__hyperframesWebglDraws;
it("names the scene whose canvas draws while its clip is hidden", () => {
drawn().add(mountScenes(true));
const found = runAudit().filter((issue) => issue.code === "webgl_draws_while_inactive");
expect(found).toHaveLength(1);
expect(found[0]).toMatchObject({ severity: "warning", selector: "#shader" });
expect(found[0]?.message).toContain("scene2");
});
it("stays quiet for a canvas drawing on screen, and reports a draw once", () => {
drawn().add(mountScenes(false));
expect(runAudit().some((issue) => issue.code === "webgl_draws_while_inactive")).toBe(false);
drawn().add(document.getElementById("shader") as HTMLCanvasElement);
document.getElementById("scene2")?.setAttribute("data-hf-clip-hidden", "");
expect(runAudit().filter((i) => i.code === "webgl_draws_while_inactive")).toHaveLength(1);
expect(runAudit().some((issue) => issue.code === "webgl_draws_while_inactive")).toBe(false);
});
});
describe("canvas_content_at_edge", () => {
// One grey RGBA pixel per entry: a 1px-thick band, read the same along every edge.
function edgeLine(levels: number[]): Uint8ClampedArray {
+1
View File
@@ -2189,6 +2189,7 @@ const LAYOUT_ISSUE_CODES: readonly LayoutIssueCode[] = [
"escaped_container",
"panel_out_of_canvas",
"canvas_content_at_edge",
"webgl_draws_while_inactive",
"connector_detached",
"connector_orphan",
"rotation_pivot_drift",
+1
View File
@@ -27,6 +27,7 @@ export type LayoutIssueCode =
| "escaped_container"
| "panel_out_of_canvas"
| "canvas_content_at_edge"
| "webgl_draws_while_inactive"
| "connector_detached"
| "connector_orphan"
// Cross-sample rotation finding — a spinning element whose bbox center drifts