diff --git a/tools/trace_replay/render_retrace_summary.mjs b/tools/trace_replay/render_retrace_summary.mjs
index 3829fea8..7273aa17 100644
--- a/tools/trace_replay/render_retrace_summary.mjs
+++ b/tools/trace_replay/render_retrace_summary.mjs
@@ -321,6 +321,10 @@ function htmlEscape(value) {
.replaceAll('"', """);
}
+function jsonScriptText(value) {
+ return JSON.stringify(value).replaceAll("<", "\\u003c");
+}
+
async function embedPng(source) {
if (!source) return "";
if (!(await isUsablePng(source))) return "";
@@ -364,11 +368,34 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
});
}
- const thumb = (src, label) => `
+ const imageTemplates = [];
+ const imageTemplatesByGroup = new Map();
+ const imageTemplateForGroup = (group) => {
+ let template = imageTemplatesByGroup.get(group);
+ if (!template) {
+ template = { id: `image-data-${imageTemplates.length}`, images: {}, nextImage: 0 };
+ imageTemplatesByGroup.set(group, template);
+ imageTemplates.push(template);
+ }
+ return template;
+ };
+ const registerImage = (group, src) => {
+ if (!src) return "";
+ const template = imageTemplateForGroup(group);
+ const id = `image-${template.nextImage}`;
+ template.nextImage += 1;
+ template.images[id] = src;
+ return id;
+ };
+
+ const thumb = (group, src, label) => {
+ const imageId = registerImage(group, src);
+ return `
${htmlEscape(label)}
- ${src ? `` : `NO IMAGE
`}
+ ${imageId ? `` : `NO IMAGE
`}
`;
+ };
const missingImages = (row) => !row.actualSrc || !row.goldenSrc || !row.diffSrc;
const cellFor = (group, backend, caseName) => cells.get(`${group}\0${backend}\0${caseName}`) ?? {
@@ -387,9 +414,9 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
${backend} ${statusText(row)}
- ${thumb(row.actualSrc, "actual")}
- ${thumb(row.goldenSrc, "golden")}
- ${thumb(row.diffSrc, "diff")}
+ ${thumb(group, row.actualSrc, "actual")}
+ ${thumb(group, row.goldenSrc, "golden")}
+ ${thumb(group, row.diffSrc, "diff")}
`;
};
@@ -432,7 +459,7 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
}
- main { width: 1170px; margin: 0 auto; padding: 14px 10px 96px; }
+ main { width: min(1170px, 100%); margin: 0 auto; padding: 14px 10px 96px; }
.document-title {
margin: 0 0 6px;
font-size: 28px;
@@ -448,6 +475,28 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
align-items: start;
justify-content: space-between;
gap: 16px;
+ margin-bottom: 10px;
+ padding: 12px 14px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #2f2f2f;
+ cursor: pointer;
+ user-select: none;
+ }
+ .group-title:hover { border-color: #5a6473; }
+ .group-title:focus-visible {
+ outline: 2px solid #6f87ff;
+ outline-offset: 2px;
+ }
+ .group-title::after {
+ content: "+ Expand";
+ flex: 0 0 auto;
+ align-self: center;
+ color: var(--muted);
+ font-weight: 700;
+ }
+ .group:not(.collapsed) .group-title::after {
+ content: "- Collapse";
}
h1 { margin: 0; font-size: 24px; line-height: 1.2; }
p { margin: 4px 0 12px; color: var(--muted); }
@@ -506,23 +555,13 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
width: 100%;
background: #252b38;
}
- .toggle-cases {
- flex: 0 0 auto;
- margin-top: 2px;
- border: 1px solid var(--line);
- border-radius: 6px;
- padding: 6px 10px;
- color: var(--text);
- background: #151922;
- font: inherit;
- cursor: pointer;
- }
- .toggle-cases:hover { background: #1b2130; }
.group.collapsed .case-list { display: none; }
+ .case-list { overflow-x: auto; }
.grid {
display: grid;
grid-template-columns: 280px 430px 430px;
column-gap: 10px;
+ min-width: 1150px;
}
.header-row {
color: var(--muted);
@@ -623,20 +662,54 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
position: fixed;
inset: 0;
z-index: 20;
- display: grid;
- place-items: center;
- padding: 28px;
+ display: flex;
+ flex-direction: column;
+ padding: max(12px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
background: rgb(0 0 0 / 86%);
cursor: zoom-out;
}
.lightbox[hidden] {
display: none;
}
+ .lightbox-close {
+ position: fixed;
+ right: max(10px, env(safe-area-inset-right));
+ top: max(10px, env(safe-area-inset-top));
+ z-index: 21;
+ width: 38px;
+ height: 38px;
+ border: 1px solid rgb(255 255 255 / 24%);
+ border-radius: 999px;
+ color: var(--text);
+ background: rgb(18 18 18 / 92%);
+ font-size: 24px;
+ line-height: 34px;
+ cursor: pointer;
+ }
+ .lightbox-stage {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ place-items: center;
+ overflow: auto;
+ overscroll-behavior: contain;
+ -webkit-overflow-scrolling: touch;
+ padding: 44px 0 16px;
+ }
.lightbox img {
max-width: min(96vw, 1600px);
- max-height: 94vh;
+ max-height: calc(100dvh - 72px);
object-fit: contain;
box-shadow: 0 18px 70px rgb(0 0 0 / 70%);
+ cursor: zoom-in;
+ }
+ .lightbox.zoomed .lightbox-stage {
+ place-items: start center;
+ }
+ .lightbox.zoomed img {
+ max-width: none;
+ max-height: none;
+ cursor: zoom-out;
}
.summary-footer {
margin-top: 24px;
@@ -652,6 +725,34 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
.summary-footer { display: none; }
.lightbox { display: none; }
}
+ @media (max-width: 700px) {
+ body { font-size: 11px; }
+ main { padding: 10px 6px 72px; }
+ .document-title { font-size: 22px; }
+ .group-title {
+ display: block;
+ padding: 10px 10px 12px;
+ }
+ .group-title::after {
+ display: block;
+ margin-top: 8px;
+ }
+ h1 { font-size: 18px; }
+ .stats { gap: 6px; }
+ .stat { padding: 4px 6px; }
+ .status-bar { margin-bottom: 0; }
+ .lightbox {
+ background: rgb(0 0 0 / 92%);
+ }
+ .lightbox-stage {
+ padding-top: 52px;
+ align-items: start;
+ }
+ .lightbox img {
+ max-width: calc(100vw - 20px);
+ max-height: calc(100dvh - 84px);
+ }
+ }
@@ -660,8 +761,10 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
Each backend cell shows actual / golden / diff. Failed cases are red; absent results are orange; incomplete image sets get an orange notch.
`);
+ const caseTemplates = [];
for (const group of groups) {
const displayGroup = groupLabels.get(group) ?? group;
+ const templateId = `case-template-${caseTemplates.length}`;
const groupCells = CASES.flatMap((caseName) => BACKENDS.map((backend) => cellFor(group, backend, caseName)));
const passed = groupCells.filter((row) => effectiveStatus(row) === "PASS").length;
const failed = groupCells.filter((row) => effectiveStatus(row) === "FAIL").length;
@@ -672,9 +775,30 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
const passPercent = statusTotal === 0 ? "0" : (passed * 100 / statusTotal).toFixed(3);
const noResultPercent = statusTotal === 0 ? "0" : (noResult * 100 / statusTotal).toFixed(3);
const failPercent = statusTotal === 0 ? "0" : (failed * 100 / statusTotal).toFixed(3);
+ let caseHtml = `
+
+`;
+ for (const [index, caseName] of CASES.entries()) {
+ caseHtml += `
+
+
+ ${backendCell(group, "DirectGLES", caseName)}
+ ${backendCell(group, "DirectVulkan", caseName)}
+
`;
+ }
+ caseTemplates.push({ id: templateId, html: caseHtml });
+ const imageTemplate = imageTemplatesByGroup.get(group);
await write(`
-
-
+
+
${htmlEscape(displayGroup)}
@@ -689,73 +813,117 @@ async function renderHtml(rows, groupLabels, outputDir, title, groupLabel, htmlN
: ``}
-
-
-
-
-`);
- for (const [index, caseName] of CASES.entries()) {
- await write(`
-
-
- ${backendCell(group, "DirectGLES", caseName)}
- ${backendCell(group, "DirectVulkan", caseName)}
-
`);
- }
- await write(`
+
`);
}
+ for (const template of caseTemplates) {
+ await write(`
+`);
+ }
+
await write(`
-
![]()
+
+
+
![]()
+
+`);
+
+ for (const template of imageTemplates) {
+ await write(`
+`);
+ }
+
+ await write(`