⌕ Anatomy 0) link(`pe${pe - 1}`, `pe${pe}`);
+ if (row > 0) link(`pe${pe - 4}`, `pe${pe}`);
+ }
+ }
+ link("memory", "scratch");
+ link("scheduler", "compute");
+ for (let row = 0; row < 4; row++) link("scratch", `pe${row * 4}`);
+ for (let col = 0; col < 4; col++) link("scratch", `pe${col}`);
+ for (let col = 0; col < 4; col++) link(`pe${12 + col}`, "output");
+ } else {
+ for (let row = 0; row < 4; row++) {
+ for (let col = 0; col < 8; col++) {
+ const lane = row * 8 + col;
+ block(
+ `lane${lane}`,
+ `Warp lane ${lane}`,
+ -0.8 + col * 0.31,
+ -0.72 + row * 0.46,
+ 0.23,
+ 0.31,
+ `Lane ${lane} of the displayed 32-lane warp. Its output index comes from the model snapshot. These 32 cells are a selected execution group, not a physical count of GPU cores.`,
+ );
+ }
+ }
+ link("memory", "registers");
+ link("scheduler", "compute");
+ link("registers", "compute");
+ for (const lane of [0, 8, 16, 24]) link("registers", `lane${lane}`);
+ for (const lane of [7, 15, 23, 31]) link(`lane${lane}`, "output");
+ }
+ }
+
+ if (mode === "switch") {
+ for (let ingress = 0; ingress < 3; ingress++) {
+ const z = (ingress - 1) * 1.04;
+ block(
+ `ingress-${ingress}`,
+ `Ingress ${ingress + 1}`,
+ -1.57,
+ z,
+ 0.35,
+ 0.59,
+ `Input port ${ingress + 1}. Injected fixed-size packets enter the bounded shared storage here. Full capacity rejects a packet; no traffic is silently created or removed.`,
+ ingress === 1,
+ );
+ for (let egress = 0; egress < 3; egress++) {
+ block(
+ `voq-${ingress}-${egress}`,
+ `Queue ${ingress + 1} → ${egress + 1}`,
+ -0.98 + egress * 0.37,
+ z,
+ 0.26,
+ 0.43,
+ `Packets arriving at input ${ingress + 1} for output ${egress + 1}. In VOQ mode, another destination can proceed when this one is blocked. FIFO mode consolidates the input's queue at its middle cell.`,
+ );
+ link(`ingress-${ingress}`, `voq-${ingress}-${egress}`);
+ }
+ }
+ block(
+ "fabric",
+ "Grant · transfer",
+ 0.58,
+ 0,
+ 0.57,
+ 2.65,
+ "A generic rotating arbiter grants at most one transfer per input and output in a tick. Each output has one transfer slot; a blocked output retains its occupied credit. This is not a proprietary Cisco scheduler.",
+ true,
+ );
+ for (let egress = 0; egress < 3; egress++) {
+ block(
+ `egress-${egress}`,
+ `Output ${"ABC"[egress]}`,
+ 1.48,
+ (egress - 1) * 1.04,
+ 0.42,
+ 0.59,
+ `Output port ${egress + 1}. Blocking it prevents completion and new grants to this destination. Releasing it allows held work to drain. Other destinations can still proceed in VOQ mode.`,
+ true,
+ );
+ link("fabric", `egress-${egress}`);
+ for (let ingress = 0; ingress < 3; ingress++)
+ link(`voq-${ingress}-${egress}`, "fabric");
+ }
+ }
+ return { blocks, links };
+}
+
+/** Map exact model IDs, plus explicit display aliases, to selectable geometry. */
+export function resolveBlock(layout, nodeId) {
+ if (!layout || !Array.isArray(layout.blocks) || typeof nodeId !== "string") {
+ return undefined;
+ }
+ const exact = layout.blocks.find((block) => block.key === nodeId);
+ if (exact) return exact;
+ const fifo = /^fifo-([0-2])$/.exec(nodeId);
+ if (fifo)
+ return layout.blocks.find((block) => block.key === `voq-${fifo[1]}-1`);
+ // There is no additional physical scheduler or drop engine in this schematic.
+ // These aliases locate abstract events on the central switch mechanism.
+ if (nodeId === "scheduler" || nodeId === "drop") {
+ return layout.blocks.find((block) => block.key === "fabric");
+ }
+ return undefined;
+}
diff --git a/public/silicon.css b/public/silicon.css
new file mode 100644
index 0000000..f0e9580
--- /dev/null
+++ b/public/silicon.css
@@ -0,0 +1,951 @@
+@import url("fonts/fonts.css");
+:root {
+ color-scheme: dark;
+ --bg: #080808;
+ --text: #efefed;
+ --muted: #a6aeaf;
+ --line: #ffffff20;
+ --mint: #b8e4e6;
+ --sans: "DM Sans", -apple-system, BlinkMacSystemFont, sans-serif;
+ --mono: "IBM Plex Mono", monospace;
+}
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--text);
+ font: 14px var(--sans);
+ overflow: hidden;
+}
+button,
+input,
+select {
+ font: inherit;
+}
+button,
+a,
+select {
+ touch-action: manipulation;
+ -webkit-tap-highlight-color: transparent;
+}
+button {
+ border: 0;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+}
+button:hover {
+ background: #ffffff12;
+}
+button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+button:focus-visible,
+a:focus-visible,
+select:focus-visible,
+canvas:focus-visible,
+input:focus-visible {
+ outline: 2px solid var(--mint);
+ outline-offset: 4px;
+}
+a {
+ color: inherit;
+ text-underline-offset: 4px;
+}
+select {
+ border: 1px solid var(--line);
+ background: #111616;
+ color: var(--text);
+ border-radius: 8px;
+ padding: 7px 10px;
+ max-width: 100%;
+}
+option {
+ background: #101414;
+}
+button {
+ border-radius: 9px;
+ padding: 9px 12px;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+}
+[hidden] {
+ display: none !important;
+}
+.skip {
+ position: fixed;
+ top: -50px;
+ left: 10px;
+ z-index: 100;
+}
+.skip:focus {
+ top: 10px;
+ background: #111;
+ padding: 10px;
+}
+header {
+ position: fixed;
+ top: 22px;
+ left: 32px;
+ right: 32px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 20px;
+ z-index: 10;
+ pointer-events: none;
+}
+header > * {
+ pointer-events: auto;
+}
+.brand {
+ font-size: 32px;
+ font-weight: 650;
+ letter-spacing: -1.8px;
+ text-decoration: none;
+}
+.brand span {
+ font-size: 20px;
+ color: var(--mint);
+ margin-left: 2px;
+}
+.worlds {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ border: 1px solid var(--line);
+ border-radius: 28px;
+ background: #0d1111de;
+ padding: 5px;
+}
+.worlds a {
+ padding: 10px 19px;
+ border-radius: 22px;
+ text-decoration: none;
+ font-size: 12px;
+ color: #aeb5b5;
+}
+.worlds a[aria-current="page"] {
+ background: #d8e3e11c;
+ color: #fff;
+}
+.quiet {
+ font-size: 12px;
+ color: #c5cdcd;
+}
+.stage {
+ position: fixed;
+ inset: 0;
+}
+#chip-canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+ touch-action: none;
+ cursor: grab;
+}
+#chip-canvas:active {
+ cursor: grabbing;
+}
+#chip-labels {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ overflow: hidden;
+}
+.chip-label {
+ position: absolute;
+ transform: translate(-50%, -50%);
+ font: 10px var(--mono);
+ color: #d9e1e2;
+ text-shadow: 0 1px 6px #000;
+ background: #080c0dc9;
+ border: 1px solid #bacbd21c;
+ border-radius: 5px;
+ padding: 4px 7px;
+ white-space: nowrap;
+}
+.chip-heading {
+ position: absolute;
+ top: 112px;
+ left: 36px;
+ max-width: 630px;
+}
+.eyebrow {
+ font: 10px var(--mono);
+ letter-spacing: 1.5px;
+ color: #b7cdce;
+}
+.chip-heading select {
+ display: block;
+ margin-top: 12px;
+ font-size: clamp(22px, 2.3vw, 34px);
+ letter-spacing: -1px;
+ background: transparent;
+ border: 0;
+ padding: 0 25px 0 0;
+ width: 100%;
+ outline-offset: 10px;
+}
+.chip-heading select option {
+ font-size: 16px;
+ letter-spacing: 0;
+}
+.chip-heading p {
+ font: 11px var(--mono);
+ line-height: 1.8;
+ color: var(--muted);
+ margin-top: 12px;
+ max-width: 300px;
+}
+.view-tools {
+ position: absolute;
+ top: 112px;
+ right: 32px;
+ display: flex;
+ border: 1px solid var(--line);
+ border-radius: 25px;
+ background: #111616be;
+ padding: 3px;
+}
+.view-tools button {
+ font-size: 19px;
+ border-radius: 22px;
+ min-width: 40px;
+}
+.scene-note {
+ position: absolute;
+ right: 35px;
+ bottom: 255px;
+ color: #bac5c6;
+ font: 11px var(--mono);
+ max-width: 240px;
+ line-height: 1.8;
+ text-align: right;
+}
+.glass {
+ border: 1px solid #d6ece92b;
+ background: linear-gradient(135deg, #151c1dee, #0e1213ed);
+ border-radius: 20px;
+ box-shadow: 0 15px 60px #0004;
+ backdrop-filter: blur(18px);
+}
+.input-card {
+ position: fixed;
+ left: 32px;
+ top: 285px;
+ width: 212px;
+ padding: 16px;
+ z-index: 4;
+}
+.card-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 11px;
+}
+.card-heading .eyebrow {
+ font-size: 9px;
+ letter-spacing: 1px;
+}
+.card-heading button {
+ padding: 0 5px;
+ font-size: 20px;
+}
+#input-tile {
+ width: 178px;
+ height: 178px;
+ border-radius: 5px;
+ display: block;
+ touch-action: none;
+ cursor: crosshair;
+}
+.input-caption {
+ display: flex;
+ justify-content: space-between;
+ font: 8px var(--mono);
+ color: var(--muted);
+ margin: 10px 0 13px;
+}
+.input-card select {
+ width: 100%;
+ font-size: 11px;
+}
+.small {
+ font-size: 11px;
+ color: var(--muted);
+ line-height: 1.65;
+}
+.input-card .small {
+ margin: 12px 0 0;
+}
+.output-card {
+ position: fixed;
+ right: 36px;
+ top: 250px;
+ width: 145px;
+ z-index: 3;
+ text-align: right;
+}
+.output-card .eyebrow {
+ font-size: 9px;
+}
+.output-card canvas {
+ display: block;
+ width: 112px;
+ height: 112px;
+ margin: 16px 0 10px auto;
+ border-radius: 4px;
+}
+.output-card .small {
+ font-family: var(--mono);
+ font-size: 9px;
+}
+.input-card h2 {
+ font-size: 23px;
+ font-weight: 500;
+ letter-spacing: -0.6px;
+ line-height: 1.2;
+ margin: 15px 0;
+}
+.input-card > p {
+ font-size: 12px;
+ line-height: 1.7;
+ color: #bac7c8;
+}
+.packet-actions,
+.valves {
+ display: flex;
+ gap: 3px;
+ margin: 14px -4px;
+}
+.packet-actions button,
+.valves button {
+ font-size: 10px;
+ padding: 9px 5px;
+ flex: 1;
+ border: 1px solid var(--line);
+}
+.valves button[aria-pressed="true"] {
+ color: #f3c48d;
+ border-color: #f3c48d70;
+ background: #f3c48d12;
+}
+.bottom-area {
+ position: fixed;
+ bottom: 38px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: min(840px, calc(100% - 64px));
+ z-index: 6;
+}
+.stats {
+ display: flex;
+ align-items: end;
+ justify-content: center;
+ gap: 37px;
+ margin: 0 auto 22px;
+}
+.stats > div {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 85px;
+}
+.stats span {
+ font-size: 29px;
+ line-height: 1;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: -1px;
+}
+.stats small {
+ font: 9px var(--mono);
+ color: #afbbbc;
+}
+.model-link {
+ font-size: 11px;
+ color: #c4d3d5;
+ margin-left: 20px;
+}
+.control-dock {
+ padding: 12px 18px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px 15px;
+}
+.families,
+.run-controls {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+}
+.families button {
+ font-size: 12px;
+ padding: 11px 13px;
+ white-space: nowrap;
+}
+.families button.active {
+ background: #d5e5e41c;
+ color: #fff;
+}
+.run-controls {
+ justify-content: flex-end;
+}
+.run-controls button {
+ font-size: 11px;
+ padding: 10px 11px;
+ white-space: nowrap;
+}
+.run-controls .primary {
+ background: #d5e8e6;
+ color: #102326;
+ min-width: 85px;
+}
+.run-controls .primary:hover {
+ background: #effffb;
+}
+.speed-label {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 10px;
+ color: var(--muted);
+ margin-left: 4px;
+}
+.speed-label select {
+ padding: 5px;
+ font-size: 10px;
+ border: 0;
+ background: #ffffff09;
+}
+.model-controls {
+ grid-column: 1/-1;
+ border-top: 1px solid var(--line);
+ padding-top: 10px;
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+.model-controls label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 10px;
+ color: #cad3d3;
+}
+.model-controls select {
+ font-size: 10px;
+ padding: 4px 6px;
+}
+.model-controls #run-status {
+ margin-left: auto;
+ font: 9px var(--mono);
+ color: #c7d4d5;
+ text-align: right;
+ max-width: 300px;
+}
+.inspector {
+ position: fixed;
+ left: 32px;
+ top: 230px;
+ bottom: 250px;
+ width: 350px;
+ z-index: 9;
+ display: flex;
+ flex-direction: column;
+ max-height: 650px;
+ overflow: hidden;
+}
+.inspector-top {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ border-bottom: 1px solid var(--line);
+ gap: 5px;
+}
+.inspector-top > button {
+ margin-left: auto;
+}
+.inspector-top [role="tablist"] {
+ display: flex;
+ overflow: auto;
+ gap: 4px;
+ scrollbar-width: thin;
+}
+.inspector-top [role="tab"] {
+ font-size: 10px;
+ white-space: nowrap;
+ border-radius: 7px;
+}
+.inspector-top [aria-selected="true"] {
+ background: #d3e4e31c;
+}
+.inspector #inspection {
+ padding: 20px;
+ overflow: auto;
+ line-height: 1.7;
+ font-size: 12px;
+}
+.inspector h2 {
+ font-size: 25px;
+ font-weight: 500;
+ letter-spacing: -0.7px;
+ line-height: 1.2;
+ margin: 10px 0 15px;
+}
+.inspector h3 {
+ font-size: 13px;
+ font-weight: 500;
+ margin-top: 23px;
+}
+.inspector p {
+ color: #bbc6c7;
+}
+.inspector dl {
+ margin: 10px 0;
+}
+.inspector dt {
+ font-size: 10px;
+ color: #a4b3b6;
+ margin-top: 14px;
+}
+.inspector dd {
+ margin: 3px 0 0;
+ font-size: 14px;
+}
+.source-link {
+ display: block;
+ text-align: left;
+ width: 100%;
+ padding: 10px 0;
+ border-top: 1px solid var(--line);
+ border-radius: 0;
+ font-size: 11px;
+ color: #d4e2e3;
+}
+.inspector .limit {
+ border-left: 2px solid #9ebcbe6b;
+ padding-left: 12px;
+ font-size: 11px;
+}
+.inspector table {
+ width: 100%;
+ font-size: 10px;
+ border-collapse: collapse;
+}
+.inspector td,
+.inspector th {
+ padding: 7px 3px;
+ border-bottom: 1px solid var(--line);
+ text-align: left;
+ font-weight: 400;
+}
+.display-settings {
+ position: fixed;
+ right: 32px;
+ top: 170px;
+ width: 240px;
+ z-index: 12;
+ padding: 20px;
+}
+.display-settings label {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ font-size: 11px;
+ margin-bottom: 16px;
+}
+.display-settings select {
+ max-width: 145px;
+ font-size: 10px;
+}
+.display-settings input {
+ accent-color: #a5d3d3;
+}
+#loading {
+ position: absolute;
+ top: 48%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font: 12px var(--mono);
+ color: #b4ced0;
+ text-align: center;
+ max-width: 400px;
+}
+#loading.error {
+ border: 1px solid var(--line);
+ background: #0c1112e8;
+ border-radius: 12px;
+ padding: 20px;
+}
+#toast {
+ position: fixed;
+ top: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 30;
+ border: 1px solid var(--line);
+ background: #162021;
+ border-radius: 20px;
+ padding: 9px 16px;
+ font-size: 11px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.15s;
+}
+#toast.show {
+ opacity: 1;
+}
+footer {
+ position: fixed;
+ bottom: 12px;
+ left: 32px;
+ right: 32px;
+ display: flex;
+ justify-content: space-between;
+ font: 8px var(--mono);
+ letter-spacing: 0.5px;
+ color: #9aa9aa;
+ pointer-events: none;
+}
+#hdr-text {
+ position: fixed;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 20;
+}
+@media (max-width: 1050px) {
+ header {
+ left: 22px;
+ right: 22px;
+ }
+ .quiet {
+ font-size: 10px;
+ }
+ .input-card {
+ left: 22px;
+ width: 180px;
+ top: 250px;
+ padding: 13px;
+ }
+ #input-tile {
+ width: 152px;
+ height: 152px;
+ }
+ .input-caption {
+ font-size: 7px;
+ }
+ .chip-heading {
+ left: 25px;
+ top: 115px;
+ max-width: 500px;
+ }
+ .output-card {
+ right: 24px;
+ width: 120px;
+ }
+ .output-card canvas {
+ width: 88px;
+ height: 88px;
+ }
+ .scene-note {
+ display: none;
+ }
+ .bottom-area {
+ width: calc(100% - 44px);
+ }
+ .inspector {
+ left: 22px;
+ top: 215px;
+ bottom: 245px;
+ }
+ .speed-label {
+ display: none;
+ }
+ .model-link {
+ margin-left: 0;
+ }
+ .families button {
+ padding: 10px;
+ }
+ .control-dock {
+ gap: 8px;
+ padding: 12px;
+ }
+}
+@media (max-width: 700px) {
+ body {
+ font-size: 12px;
+ }
+ header {
+ top: 16px;
+ left: 18px;
+ right: 18px;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+ .brand {
+ font-size: 29px;
+ }
+ .worlds {
+ order: 3;
+ width: 100%;
+ justify-content: center;
+ padding: 3px;
+ }
+ .worlds a {
+ font-size: 11px;
+ padding: 8px 15px;
+ }
+ .quiet {
+ padding: 5px;
+ font-size: 10px;
+ }
+ .chip-heading {
+ top: 122px;
+ left: 22px;
+ max-width: calc(100% - 100px);
+ }
+ .chip-heading select {
+ font-size: 23px;
+ margin-top: 8px;
+ }
+ .chip-heading .eyebrow {
+ font-size: 8px;
+ }
+ .chip-heading p {
+ font-size: 8px;
+ max-width: 240px;
+ margin-top: 9px;
+ }
+ .view-tools {
+ top: 126px;
+ right: 16px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ padding: 1px;
+ border-radius: 13px;
+ }
+ .view-tools button {
+ font-size: 16px;
+ padding: 5px;
+ min-width: 28px;
+ }
+ .input-card {
+ top: 230px;
+ left: 16px;
+ width: 118px;
+ padding: 10px;
+ border-radius: 13px;
+ }
+ .card-heading {
+ margin-bottom: 5px;
+ }
+ .card-heading .eyebrow {
+ font-size: 7px;
+ letter-spacing: 0.1px;
+ }
+ .card-heading button {
+ font-size: 16px;
+ padding: 0;
+ }
+ #input-tile {
+ width: 96px;
+ height: 96px;
+ }
+ .input-caption {
+ font-size: 7px;
+ display: block;
+ line-height: 1.7;
+ margin: 6px 0;
+ }
+ .input-caption span:last-child {
+ display: none;
+ }
+ .input-card select {
+ font-size: 9px;
+ padding: 5px;
+ }
+ .input-card .small {
+ display: none;
+ }
+ .output-card {
+ right: 17px;
+ top: 235px;
+ width: 85px;
+ }
+ .output-card .eyebrow {
+ font-size: 7px;
+ letter-spacing: 0.7px;
+ }
+ .output-card canvas {
+ width: 68px;
+ height: 68px;
+ margin: 11px 0 5px auto;
+ }
+ .output-card .small {
+ font-size: 7px;
+ }
+ .bottom-area {
+ bottom: 34px;
+ width: calc(100% - 24px);
+ }
+ .stats {
+ gap: 22px;
+ margin-bottom: 16px;
+ justify-content: center;
+ }
+ .stats > div {
+ min-width: 66px;
+ }
+ .stats span {
+ font-size: 24px;
+ }
+ .stats small {
+ font-size: 8px;
+ }
+ .model-link {
+ display: none;
+ }
+ .control-dock {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ padding: 9px 11px;
+ border-radius: 18px;
+ }
+ .families {
+ justify-content: space-between;
+ }
+ .families button {
+ font-size: 11px;
+ flex: 1;
+ padding: 9px 5px;
+ }
+ .run-controls {
+ justify-content: center;
+ gap: 5px;
+ }
+ .run-controls button {
+ font-size: 11px;
+ padding: 9px 14px;
+ }
+ .run-controls .primary {
+ flex: 1;
+ }
+ .model-controls {
+ gap: 7px;
+ padding-top: 8px;
+ }
+ .model-controls label {
+ font-size: 9px;
+ gap: 4px;
+ }
+ .model-controls select {
+ font-size: 9px;
+ padding: 4px;
+ }
+ .model-controls #run-status {
+ font-size: 8px;
+ max-width: 142px;
+ }
+ .inspector {
+ left: 14px;
+ right: 14px;
+ width: auto;
+ top: 213px;
+ bottom: 255px;
+ max-height: none;
+ }
+ .inspector #inspection {
+ padding: 15px;
+ font-size: 11px;
+ }
+ .inspector h2 {
+ font-size: 23px;
+ }
+ .display-settings {
+ right: 15px;
+ top: 192px;
+ width: 225px;
+ }
+ #packet-card {
+ width: 170px;
+ top: 230px;
+ }
+ #packet-card h2 {
+ font-size: 17px;
+ margin: 8px 0;
+ }
+ #packet-card > p {
+ font-size: 10px;
+ margin: 8px 0;
+ }
+ .packet-actions,
+ .valves {
+ margin: 9px -3px;
+ gap: 2px;
+ }
+ .packet-actions button,
+ .valves button {
+ font-size: 9px;
+ padding: 7px 3px;
+ }
+ footer {
+ left: 15px;
+ right: 15px;
+ font-size: 6px;
+ letter-spacing: 0;
+ }
+ footer span:first-child {
+ max-width: 200px;
+ }
+ #toast {
+ top: 100px;
+ width: max-content;
+ max-width: 90%;
+ font-size: 10px;
+ padding: 6px 12px;
+ }
+ #loading {
+ font-size: 10px;
+ width: 90%;
+ }
+}
+@media (max-height: 740px) and (min-width: 701px) {
+ .input-card {
+ top: 238px;
+ }
+ .output-card {
+ top: 235px;
+ }
+ .input-card .small {
+ display: none;
+ }
+ .bottom-area {
+ bottom: 30px;
+ }
+ .stats {
+ margin-bottom: 12px;
+ }
+ .inspector {
+ top: 218px;
+ bottom: 230px;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ * {
+ transition: none !important;
+ }
+}
diff --git a/public/silicon.html b/public/silicon.html
new file mode 100644
index 0000000..dfb269d
--- /dev/null
+++ b/public/silicon.html
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+ doke
+
+
+
+
+ Skip to simulation controls
+
+
+
+
+
+ Opening the silicon world…
+
+
PARALLEL COMPUTE
+
Reference processor
+
+ A magnified teaching model · original schematic
+
+
+
+ + − ↺ ⋯
+
+
+ Your input. Every read. Every result.
+
+
+
+
+ THE RESULT
+
+ 0 / 64 values ready
+
+
+
+
+
+
+ 0 model ticks
+
+ 0 useful MACs
+
+
+ 0 bytes moved
+
+
+ What is calculated? ↗
+
+
+
+
+ CPU GPU Tensor Switch ASIC
+
+
+ Run ↗ Step Reset
+ ⇥ Pace
+ Slow
+ Flow
+ Fast
+
+
+
+ Memory layout
+ Packed
+ Strided
+ Tiny
+ cacheQueues
+ FIFO
+ Per destination · VOQ
+ Ready to follow your data
+
+
+
+
+
+
+ PUBLIC CHIP REFERENCES · EXECUTABLE TEACHING MODELS Preparing renderer
+
+
+
+
diff --git a/public/silicon.js b/public/silicon.js
new file mode 100644
index 0000000..22807c8
--- /dev/null
+++ b/public/silicon.js
@@ -0,0 +1,806 @@
+import { hdrCapable, configureHDR, HDRText } from "./display.js";
+import { makeLayout, resolveBlock } from "./silicon-layouts.js";
+const $ = (s) => document.querySelector(s),
+ $$ = (s) => [...document.querySelectorAll(s)];
+const escape = (s) =>
+ String(s ?? "").replace(
+ /[&<>"']/g,
+ (c) =>
+ ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
+ c
+ ],
+ );
+let wasm, renderer, hdrText, catalog, model, snap, layout, profile;
+let family = "gpu",
+ playing = false,
+ reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
+let last = 0,
+ accumulator = 0,
+ tickPhase = 0.5,
+ dirty = true,
+ drag,
+ selected = null,
+ paint = false,
+ brush = 0;
+let input = pattern("diagonal"),
+ panels = new Map(),
+ currentPanel,
+ nextModel = 0;
+let canvas = $("#chip-canvas");
+const panel = $("#inspector");
+const pretty = (n) => Number(n).toLocaleString();
+const byFamily = { cpu: "cpu", gpu: "gpu", tensor: "npu", switch: "asic" };
+const names = {
+ cpu: "GENERAL COMPUTE",
+ gpu: "PARALLEL COMPUTE",
+ tensor: "TENSOR COMPUTE",
+ switch: "PACKET SWITCHING",
+};
+const tickRates = { cpu: 150, gpu: 18, tensor: 24, switch: 4 };
+
+function pattern(id) {
+ return Array.from({ length: 64 }, (_, i) => {
+ const r = i >> 3,
+ c = i % 8;
+ return id === "clear"
+ ? 0
+ : id === "stripes"
+ ? c % 3 === 1
+ ? 7
+ : 0
+ : id === "checker"
+ ? (r + c) % 2
+ ? 7
+ : 0
+ : Math.abs(r - c) < 2
+ ? 7
+ : 0;
+ });
+}
+function toast(message) {
+ $("#toast").textContent = message;
+ $("#toast").classList.add("show");
+ clearTimeout(toast.timer);
+ toast.timer = setTimeout(() => $("#toast").classList.remove("show"), 2400);
+}
+function pause() {
+ playing = false;
+ accumulator = 0;
+ updateControls();
+ if (snap?.tick && snap.status !== "complete" && family !== "switch")
+ $("#run-status").textContent = "Paused · inspect any block";
+}
+function updateControls() {
+ $("#run").textContent = playing
+ ? "Pause Ⅱ"
+ : snap?.status === "complete"
+ ? "Replay ↗"
+ : "Run ↗";
+ $("#run").setAttribute("aria-pressed", String(playing));
+ for (const id of ["run", "step", "reset", "finish"])
+ $("#" + id).disabled = !model;
+ $("#finish").hidden = family === "switch";
+ $("#step").disabled = !model || snap?.status === "complete";
+}
+function resetModel() {
+ if (!wasm) return;
+ pause();
+ model?.free();
+ if (family === "switch")
+ model = new wasm.FabricSimulation($("#queue-mode").value);
+ else
+ model = new wasm.SiliconSimulation(
+ JSON.stringify({
+ mode: family,
+ input,
+ filter: "difference",
+ layout: $("#memory-layout").value,
+ cache_enabled: $("#cache-enabled").checked,
+ issue_width: 2,
+ }),
+ );
+ snap = JSON.parse(model.snapshot());
+ nextModel++;
+ tickPhase = 0.5;
+ selected = null;
+ renderer?.select(-1);
+ updateSnapshot();
+}
+function snapshotFrom(result) {
+ snap = JSON.parse(result);
+ updateSnapshot();
+ return snap;
+}
+function advance(n = 1) {
+ if (!model) return;
+ snapshotFrom(model.advance(n));
+ if (snap.status === "complete") {
+ pause();
+ $("#run-status").textContent = "Complete · exact result ready";
+ }
+}
+function finish() {
+ pause();
+ if (family === "switch") return snap;
+ for (let i = 0; i < 400 && snap.status !== "complete"; i++)
+ snap = JSON.parse(model.advance(256));
+ if (snap.status !== "complete")
+ throw Error("Model exceeded its bounded run budget");
+ tickPhase = 1;
+ updateSnapshot();
+ return snap;
+}
+function updateSnapshot() {
+ if (!snap) return;
+ $("#tick-count").textContent = pretty(snap.tick);
+ if (family === "switch") {
+ $("#work-count").textContent = pretty(snap.delivered);
+ $("#traffic-count").textContent = `${snap.occupied} / 32`;
+ $("#work-label").textContent = "packets delivered";
+ $("#traffic-label").textContent = "packet slots used";
+ $("#run-status").textContent =
+ `${snap.buffered} queued · ${snap.dropped} dropped`;
+ for (let i = 0; i < 3; i++) {
+ const b = $("#block-" + i);
+ b.setAttribute("aria-pressed", String(snap.blocked[i]));
+ b.textContent = `${snap.blocked[i] ? "Open" : "Block"} ${"ABC"[i]}`;
+ }
+ } else {
+ $("#work-count").textContent = pretty(snap.counters.macs);
+ $("#traffic-count").textContent = pretty(snap.counters.bytes_moved);
+ $("#work-label").textContent = "useful MACs";
+ $("#traffic-label").textContent = "modeled bytes moved";
+ $("#output-status").textContent =
+ `${snap.counters.completed_outputs} / 64 values ready`;
+ $("#run-status").textContent =
+ snap.status === "complete"
+ ? "Complete · exact result ready"
+ : playing
+ ? "Following the computation"
+ : snap.tick
+ ? "Paused · inspect any block"
+ : "Ready to follow your data";
+ $("#scene-note").textContent =
+ family === "gpu"
+ ? `One 32-lane warp in view${snap.group_index == null ? "" : ` · warp ${snap.group_index + 1} of 2`}`
+ : family === "tensor"
+ ? `A 4 × 4 operand wave${snap.group_index == null ? "" : ` · tile ${snap.group_index + 1} of 4`}`
+ : "A small scheduler. Real operand dependencies.";
+ }
+ updateControls();
+ drawTiles();
+ dirty = true;
+ if (currentPanel?.startsWith("block:") && !panel.hidden)
+ renderBlock(
+ layout.blocks.find((b) => "block:" + b.key === currentPanel),
+ false,
+ );
+ window.dispatchEvent(
+ new CustomEvent("doke-silicon-state", {
+ detail: { snapshot: snap, input: [...input], family },
+ }),
+ );
+}
+function drawTiles() {
+ const draw = (c, values, valid, editable) => {
+ const ctx = c.getContext("2d"),
+ s = c.width / 8;
+ ctx.clearRect(0, 0, c.width, c.height);
+ for (let i = 0; i < 64; i++) {
+ const v = values[i] || 0,
+ open = !valid || valid[i];
+ ctx.fillStyle = !open
+ ? "#121819"
+ : v < 0
+ ? `rgb(${35 + Math.min(8, -v) * 12},${46 + Math.min(8, -v) * 7},${54 + Math.min(8, -v) * 4})`
+ : `rgb(${27 + v * 26},${34 + v * 26},${35 + v * 25})`;
+ ctx.fillRect((i % 8) * s + 1, (i >> 3) * s + 1, s - 2, s - 2);
+ if (editable && document.activeElement === c && i === brush) {
+ ctx.strokeStyle = "#ecfff8";
+ ctx.lineWidth = 2;
+ ctx.strokeRect((i % 8) * s + 2, (i >> 3) * s + 2, s - 4, s - 4);
+ }
+ }
+ };
+ draw($("#input-tile"), input, null, true);
+ draw(
+ $("#output-tile"),
+ snap?.output || [],
+ snap?.valid || Array(64).fill(false),
+ false,
+ );
+}
+function editAt(e) {
+ const r = $("#input-tile").getBoundingClientRect(),
+ x = Math.floor(((e.clientX - r.left) / r.width) * 8),
+ y = Math.floor(((e.clientY - r.top) / r.height) * 8);
+ if (x < 0 || x > 7 || y < 0 || y > 7) return;
+ brush = y * 8 + x;
+ input[brush] = paint.value;
+ resetModel();
+}
+function setProfile(id, reset = true) {
+ profile =
+ catalog.profiles.find((p) => p.id === id) ||
+ catalog.profiles.find((p) => p.family === byFamily[family]);
+ $("#chip-profile").value = profile.id;
+ $("#family-label").textContent = names[family];
+ $("#model-caption").textContent =
+ family === "switch"
+ ? "Generic queue model · reference product facts"
+ : family === "gpu"
+ ? "One teaching warp · reference product facts"
+ : family === "tensor"
+ ? "Small teaching array · reference product facts"
+ : "Small teaching scheduler · reference product facts";
+ if (reset) resetModel();
+ if (currentPanel === "reference" && !panel.hidden) showReference();
+ dirty = true;
+}
+function changeFamily(value) {
+ family = value;
+ pause();
+ panels.clear();
+ closePanel();
+ $$(".families button").forEach((b) => {
+ b.classList.toggle("active", b.dataset.family === family);
+ b.setAttribute("aria-pressed", String(b.dataset.family === family));
+ });
+ $("#packet-card").hidden = family !== "switch";
+ $("#input-card").hidden = family === "switch";
+ $("#output-card").hidden = family === "switch";
+ $("#layout-control").hidden = !["cpu", "gpu"].includes(family);
+ $("#cache-control").hidden = family !== "cpu";
+ $("#queue-control").hidden = family !== "switch";
+ $("#chip-profile").replaceChildren(
+ ...catalog.profiles
+ .filter((p) => p.family === byFamily[family])
+ .map(
+ (p) =>
+ new Option(
+ p.name.replace(/^NVIDIA (GeForce )?/, "").replace(/^Cisco /, ""),
+ p.id,
+ ),
+ ),
+ );
+ layout = makeLayout(family);
+ renderer?.chip_scene(JSON.stringify(layout));
+ $("#chip-labels").replaceChildren(
+ ...layout.blocks
+ .filter((b) => b.label)
+ .map((b) => {
+ const el = document.createElement("span");
+ el.className = "chip-label";
+ el.dataset.region = b.region;
+ el.textContent = b.title;
+ return el;
+ }),
+ );
+ setProfile($("#chip-profile").value);
+ if (family === "switch")
+ $("#scene-note").textContent = "Block one output. Let the others through.";
+ history.replaceState(
+ null,
+ "",
+ `${location.pathname}${location.search}${"#" + family}`,
+ );
+}
+function showPanel(id, title, html) {
+ if (!panels.has(id) && panels.size >= 7)
+ panels.delete(panels.keys().next().value);
+ panels.set(id, { title, html });
+ currentPanel = id;
+ if (id.startsWith("block:")) {
+ selected = layout.blocks.find((b) => "block:" + b.key === id);
+ renderer?.select(selected?.region ?? -1);
+ dirty = true;
+ }
+ panel.hidden = false;
+ $("#input-card").hidden = true;
+ $("#packet-card").hidden = true;
+ $("#display-settings").hidden = true;
+ renderTabs();
+ $("#inspection").innerHTML = html;
+ if (id.startsWith("block:") && selected) renderBlock(selected, false);
+ hdrText?.redraw();
+}
+function renderTabs() {
+ $("#inspector-tabs").replaceChildren(
+ ...[...panels].map(([id, p]) => {
+ const b = document.createElement("button");
+ b.textContent = p.title;
+ b.dataset.panel = id;
+ b.setAttribute("role", "tab");
+ b.setAttribute("aria-selected", String(id === currentPanel));
+ b.onclick = () => showPanel(id, p.title, p.html);
+ return b;
+ }),
+ );
+}
+function closePanel() {
+ panel.hidden = true;
+ $("#input-card").hidden = family === "switch";
+ $("#packet-card").hidden = family !== "switch";
+ hdrText?.redraw();
+}
+function sourceButton(s) {
+ return `${escape(s.title)} ↗ `;
+}
+function showReference() {
+ showPanel(
+ "reference",
+ "Reference",
+ `PUBLIC HARDWARE REFERENCE
${escape(profile.name)} Choose a reference to inspect its documented features. The small execution model demonstrates the family’s mechanisms; these numbers do not set its timing.
${profile.facts.map((f) => `${escape(f.label)} ${escape(f.value)} `).join("")} What this view represents ${profile.limits.map((l) => `${escape(l)}
`).join("")}Follow the evidence ${profile.sources.map(sourceButton).join("")}`,
+ );
+}
+function showSource(id) {
+ const source = catalog.profiles
+ .flatMap((p) => p.sources)
+ .find((s) => s.id === id);
+ if (!source) return;
+ const facts = profile.facts.filter((f) => f.source === id);
+ showPanel(
+ "source:" + id,
+ "Source",
+ `SOURCE RECORD
${escape(source.title)} Checked ${escape(source.checked)}. Public source; original publisher retains its rights.
${facts.map((f) => `${escape(f.label)} ${escape(f.value)}
${escape(f.locator)}
`).join("")}Open the original source ↗ Product specifications are reference facts, not measurements from this model run.
`,
+ );
+}
+function showModel() {
+ const body =
+ family === "switch"
+ ? `The deterministic 3 × 3 input-queued switch holds at most 32 fixed-size packets, including transfers. One grant per input and output can occur per model tick. A blocked output holds its transfer until released.
FIFO can trap packets behind another destination. VOQ separates destinations. Changing the queue mode preserves packet identity and canonical per-input order.
Conservation ${snap.attempted} attempted = ${snap.dropped} dropped + ${snap.delivered} delivered + ${snap.occupied} held This is an original generic scheduler. It does not reproduce a Cisco pipeline, HBM buffer size, PFC or ECN implementation.
`
+ : `Your 8 × 8 matrix A is multiplied by a fixed difference matrix B. Each of 64 output cells accumulates eight signed integer products. Every mode performs 512 useful MACs and must produce the same result.
${family === "cpu" ? "Independent output dot products share a two-issue scheduler and a small LRU cache. Operands must return before a MAC. This is not x86 instruction emulation." : family === "gpu" ? "Two logical 32-lane warps issue address-based loads. Unique 32-byte sectors determine the requested traffic. The selected warp is enlarged in the schematic; no actual GPU kernel is being benchmarked." : "Four output tiles run through a 4 × 4 output-stationary array. Real operand registers pass A east and B south; each cell accumulates matching products. This is not Google’s MXU microarchitecture."}
Model ticks and memory traffic describe the declared toy rules. They are not measured chip cycles, bandwidth, energy or a prediction for this SKU.
Rules for this run ${Object.entries(
+ snap.rules,
+ )
+ .map(
+ ([k, v]) =>
+ `${escape(k.replaceAll("_", " "))} ${escape(v)} `,
+ )
+ .join("")}
`;
+ showPanel(
+ "model",
+ "Model",
+ `WHAT IS CALCULATED
${family === "switch" ? "Traffic with consequences." : "The numbers make the light."} ${body}`,
+ );
+}
+function renderBlock(block, open = true) {
+ if (!block) return;
+ const node = snap.nodes?.find((n) => n.id === block.key);
+ const packets =
+ family === "switch"
+ ? snap.packets.filter(
+ (p) => resolveBlock(layout, p.node)?.key === block.key,
+ )
+ : [];
+ const html = `INSIDE THE MODEL
${escape(block.title)} ${escape(block.description)}
${node ? `Current state ${escape(node.state)} Modeled occupancy ${node.occupancy} / ${node.capacity} ` : ""}${
+ family === "switch"
+ ? `Packets held here ${packets.length} ${packets
+ .slice(0, 12)
+ .map(
+ (p) =>
+ `Packet ${p.id} → ${"ABC"[p.egress]} · age ${p.age_ticks} ticks
`,
+ )
+ .join("")}`
+ : ""
+ }Position and size make the functional relationships legible. They are not the selected product’s physical floorplan.
`;
+ if (open) {
+ selected = block;
+ renderer?.select(block.region);
+ showPanel("block:" + block.key, block.title, html);
+ } else {
+ $("#inspection").innerHTML = html;
+ panels.set(currentPanel, { title: block.title, html });
+ }
+}
+function rendererState() {
+ if (!renderer || !snap) return;
+ const activity = new Float32Array(64),
+ flows = [];
+ if (family === "switch") {
+ for (const p of snap.packets) {
+ const b = resolveBlock(layout, p.node);
+ if (b) activity[b.region] = Math.min(0.95, activity[b.region] + 0.2);
+ }
+ for (const e of snap.events
+ .filter(
+ (e) =>
+ e.tick === snap.tick &&
+ ["transfer", "deliver", "admit"].includes(e.kind),
+ )
+ .slice(-32)) {
+ const a = resolveBlock(layout, e.from),
+ b = resolveBlock(layout, e.to);
+ if (a && b) {
+ activity[a.region] = 0.75;
+ activity[b.region] = 0.6;
+ flows.push(
+ ...[a.position[0], a.position[1] + 0.14, a.position[2]],
+ ...[b.position[0], b.position[1] + 0.14, b.position[2]],
+ tickPhase,
+ );
+ }
+ }
+ } else {
+ for (const n of snap.nodes) {
+ const b = resolveBlock(layout, n.id);
+ if (b)
+ activity[b.region] =
+ n.state === "busy"
+ ? 0.9
+ : n.occupancy
+ ? Math.min(
+ 0.7,
+ 0.2 + (0.5 * n.occupancy) / Math.max(1, n.capacity),
+ )
+ : 0.025;
+ }
+ for (const l of snap.lanes || []) {
+ const b = resolveBlock(
+ layout,
+ (family === "gpu" ? "lane" : "pe") + l.lane,
+ );
+ if (b && l.active) activity[b.region] = 0.95;
+ }
+ const active = snap.transfers
+ .filter((t) => t.start_tick <= snap.tick && t.end_tick > snap.tick)
+ .slice(-32);
+ for (const t of active) {
+ const a = resolveBlock(layout, t.from),
+ b = resolveBlock(layout, t.to);
+ if (!a || !b) continue;
+ const phase = Math.min(
+ 1,
+ (snap.tick - t.start_tick + (reduced ? 0.5 : tickPhase)) /
+ Math.max(1, t.end_tick - t.start_tick),
+ );
+ flows.push(
+ a.position[0],
+ a.position[1] + 0.14,
+ a.position[2],
+ b.position[0],
+ b.position[1] + 0.14,
+ b.position[2],
+ phase,
+ );
+ activity[a.region] = Math.max(activity[a.region], 0.35);
+ }
+ }
+ renderer.activity(activity);
+ renderer.chip_flows(new Float32Array(flows));
+}
+function labels() {
+ if (!renderer || !layout) return;
+ const r = canvas.getBoundingClientRect();
+ const obstacles = [
+ "header",
+ ".chip-heading",
+ "#input-card",
+ "#output-card",
+ "#packet-card",
+ "#inspector",
+ ".bottom-area",
+ ]
+ .map((s) => $(s))
+ .filter((e) => e && !e.hidden)
+ .map((e) => e.getBoundingClientRect());
+ $$("#chip-labels .chip-label").forEach((el) => {
+ const b = layout.blocks.find((b) => b.region === +el.dataset.region);
+ const p = renderer.project(
+ b.position[0],
+ b.position[1] + b.size[1] / 2 + 0.08,
+ b.position[2],
+ );
+ el.style.left = `${(p[0] / canvas.width) * r.width}px`;
+ el.style.top = `${(p[1] / canvas.height) * r.height}px`;
+ el.hidden = false;
+ const bounds = el.getBoundingClientRect();
+ el.hidden =
+ p[2] < 0 ||
+ p[2] > 1 ||
+ obstacles.some(
+ (o) =>
+ bounds.right > o.left &&
+ bounds.left < o.right &&
+ bounds.bottom > o.top &&
+ bounds.top < o.bottom,
+ );
+ });
+}
+function resize() {
+ const r = canvas.getBoundingClientRect(),
+ d = Math.min(devicePixelRatio || 1, 1.75);
+ canvas.width = Math.max(1, Math.round(r.width * d));
+ canvas.height = Math.max(1, Math.round(r.height * d));
+ renderer?.resize(canvas.width, canvas.height);
+ dirty = true;
+ hdrText?.redraw();
+}
+function updateLight() {
+ const light = $("#light-mode").value;
+ const active = configureHDR(canvas, renderer, light === "auto");
+ renderer?.hdr_output(active);
+ renderer?.glow(light === "quiet" ? 0.25 : 1.6);
+ if (!active && hdrText) {
+ hdrText.destroy();
+ hdrText = null;
+ }
+ if (active && !hdrText) {
+ try {
+ hdrText = new HDRText(
+ canvas.getContext("webgpu").getConfiguration().device,
+ );
+ } catch {}
+ }
+ $("#light-status").textContent = active
+ ? "Extended-range canvas active. Brightness depends on the display."
+ : "SDR contrast and bloom. Display brightness range is unchanged.";
+ hdrText?.redraw();
+ dirty = true;
+}
+function bind() {
+ $$(".families button").forEach(
+ (b) => (b.onclick = () => changeFamily(b.dataset.family)),
+ );
+ $("#chip-profile").onchange = (e) => setProfile(e.target.value);
+ $("#run").onclick = () => {
+ if (playing) pause();
+ else {
+ if (snap.status === "complete") resetModel();
+ playing = true;
+ accumulator = 0;
+ updateControls();
+ }
+ };
+ $("#step").onclick = () => {
+ pause();
+ advance(1);
+ tickPhase = 0.5;
+ };
+ $("#reset").onclick = resetModel;
+ $("#finish").onclick = finish;
+ $("#memory-layout").onchange = () => {
+ resetModel();
+ toast("Same input. A new physical address layout.");
+ };
+ $("#cache-enabled").onchange = resetModel;
+ $("#queue-mode").onchange = () => {
+ pause();
+ snapshotFrom(model.queue_mode($("#queue-mode").value));
+ };
+ for (let i = 0; i < 3; i++) {
+ $("#inject-" + i).onclick = () => snapshotFrom(model.inject(i, 6));
+ $("#block-" + i).onclick = () =>
+ snapshotFrom(model.block(i, !snap.blocked[i]));
+ }
+ $("#input-preset").onchange = (e) => {
+ input = pattern(e.target.value);
+ resetModel();
+ };
+ const tile = $("#input-tile");
+ tile.onpointerdown = (e) => {
+ if (e.button !== 0) return;
+ tile.focus();
+ const r = tile.getBoundingClientRect();
+ brush = Math.min(
+ 63,
+ Math.max(
+ 0,
+ Math.floor(((e.clientY - r.top) / r.height) * 8) * 8 +
+ Math.floor(((e.clientX - r.left) / r.width) * 8),
+ ),
+ );
+ paint = { value: input[brush] ? 0 : 7 };
+ tile.setPointerCapture(e.pointerId);
+ editAt(e);
+ };
+ tile.onpointermove = (e) => {
+ if (paint) editAt(e);
+ };
+ tile.onpointerup = () => (paint = false);
+ tile.onpointercancel = () => (paint = false);
+ tile.onkeydown = (e) => {
+ if (
+ ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", " "].includes(e.key)
+ ) {
+ e.preventDefault();
+ if (e.key === " ") {
+ input[brush] = input[brush] ? 0 : 7;
+ resetModel();
+ } else {
+ brush =
+ (brush +
+ { ArrowLeft: 63, ArrowRight: 1, ArrowUp: 56, ArrowDown: 8 }[
+ e.key
+ ]) %
+ 64;
+ drawTiles();
+ }
+ }
+ };
+ tile.onfocus = tile.onblur = drawTiles;
+ $("#hide-input").onclick = () => {
+ $("#input-body").hidden = !$("#input-body").hidden;
+ $("#hide-input").textContent = $("#input-body").hidden ? "+" : "−";
+ };
+ $("#reference").onclick = showReference;
+ $("#inspect-model").onclick = showModel;
+ $("#close-inspector").onclick = closePanel;
+ $("#inspection").onclick = (e) => {
+ const b = e.target.closest("[data-source]");
+ if (b) showSource(b.dataset.source);
+ };
+ $("#settings").onclick = () => {
+ $("#display-settings").hidden = !$("#display-settings").hidden;
+ };
+ $("#light-mode").onchange = updateLight;
+ $("#reduce-motion").checked = reduced;
+ $("#reduce-motion").onchange = (e) => {
+ reduced = e.target.checked;
+ if (reduced) pause();
+ dirty = true;
+ };
+ $("#zoom-in").onclick = () => {
+ renderer?.zoom(-0.15);
+ dirty = true;
+ };
+ $("#zoom-out").onclick = () => {
+ renderer?.zoom(0.15);
+ dirty = true;
+ };
+ $("#reset-camera").onclick = () => {
+ renderer?.view(0);
+ dirty = true;
+ };
+ canvas.onpointerdown = (e) => {
+ if (e.button !== 0) return;
+ drag = {
+ x: e.clientX,
+ y: e.clientY,
+ startX: e.clientX,
+ startY: e.clientY,
+ moved: false,
+ };
+ canvas.setPointerCapture(e.pointerId);
+ };
+ canvas.onpointermove = (e) => {
+ if (!drag) return;
+ const dx = e.clientX - drag.x,
+ dy = e.clientY - drag.y;
+ if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) > 4)
+ drag.moved = true;
+ renderer?.orbit(dx * 0.005, dy * 0.005);
+ drag.x = e.clientX;
+ drag.y = e.clientY;
+ dirty = true;
+ };
+ canvas.onpointerup = (e) => {
+ if (drag && !drag.moved && renderer) {
+ const r = canvas.getBoundingClientRect(),
+ id = renderer.pick(
+ (e.clientX - r.left) / r.width,
+ (e.clientY - r.top) / r.height,
+ );
+ renderBlock(layout.blocks.find((b) => b.region === id));
+ dirty = true;
+ }
+ drag = null;
+ };
+ canvas.onpointercancel = () => (drag = null);
+ canvas.onwheel = (e) => {
+ e.preventDefault();
+ renderer?.zoom(Math.max(-0.2, Math.min(0.2, e.deltaY * 0.001)));
+ dirty = true;
+ };
+ canvas.onkeydown = (e) => {
+ const move = {
+ ArrowLeft: [-0.1, 0],
+ ArrowRight: [0.1, 0],
+ ArrowUp: [0, -0.1],
+ ArrowDown: [0, 0.1],
+ }[e.key];
+ if (move) {
+ e.preventDefault();
+ renderer?.orbit(...move);
+ } else if (e.key === "+" || e.key === "=") renderer?.zoom(-0.15);
+ else if (e.key === "-") renderer?.zoom(0.15);
+ else if (e.key === "Enter") showModel();
+ dirty = true;
+ };
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ closePanel();
+ $("#display-settings").hidden = true;
+ }
+ });
+ document.addEventListener("visibilitychange", () => {
+ if (document.hidden) pause();
+ last = 0;
+ });
+ new ResizeObserver(resize).observe($("#silicon-stage"));
+}
+function frame(time) {
+ const dt = last ? Math.min(0.08, (time - last) / 1000) : 0;
+ last = time;
+ if (!document.hidden) {
+ if (playing && model) {
+ accumulator += dt * tickRates[family] * (+$("#pace").value / 24);
+ const count = Math.min(256, Math.floor(accumulator));
+ accumulator -= count;
+ if (count) advance(count);
+ tickPhase = accumulator;
+ dirty = true;
+ }
+ if (dirty && renderer) {
+ try {
+ rendererState();
+ renderer.render(0);
+ labels();
+ } catch (e) {
+ pause();
+ $("#render-status").textContent =
+ "Graphics interrupted · reload to restore";
+ console.warn(e);
+ renderer = null;
+ }
+ }
+ dirty = false;
+ }
+ requestAnimationFrame(frame);
+}
+async function boot() {
+ try {
+ catalog = await fetch("hardware.json").then((r) => {
+ if (!r.ok) throw Error("Reference catalog unavailable");
+ return r.json();
+ });
+ wasm = await import("./pkg/doke.js");
+ await wasm.default();
+ resize();
+ const forceGL =
+ !navigator.gpu ||
+ new URLSearchParams(location.search).get("backend") === "webgl";
+ try {
+ renderer = await wasm.BrainRenderer.create(canvas, forceGL, hdrCapable());
+ } catch (e) {
+ if (!forceGL) {
+ const replacement = canvas.cloneNode();
+ canvas.replaceWith(replacement);
+ canvas = replacement;
+ resize();
+ renderer = await wasm.BrainRenderer.create(canvas, true, false);
+ } else throw e;
+ }
+ updateLight();
+ resize();
+ $("#loading").hidden = true;
+ $("#render-status").textContent =
+ `${renderer.backend()} · original schematic`;
+ } catch (e) {
+ $("#loading").classList.add("error");
+ $("#loading").textContent =
+ `3D is unavailable. ${e.message || e} The numerical controls remain available when the model has loaded.`;
+ $("#render-status").textContent = "Numerical model · 3D unavailable";
+ console.warn(e);
+ }
+ if (wasm && catalog) {
+ bind();
+ changeFamily(
+ ["cpu", "gpu", "tensor", "switch"].includes(location.hash.slice(1))
+ ? location.hash.slice(1)
+ : "gpu",
+ );
+ }
+ document.documentElement.dataset.ready = "true";
+ requestAnimationFrame(frame);
+}
+window.dokeSilicon = {
+ snapshot: () => (snap ? structuredClone(snap) : null),
+ finish,
+ reset: resetModel,
+ input: () => [...input],
+ family: () => family,
+ inspect: showPanel,
+ getRenderer: () => renderer,
+ getWasm: () => wasm,
+};
+boot();
diff --git a/public/style.css b/public/style.css
index a54672d..d2bc1a9 100644
--- a/public/style.css
+++ b/public/style.css
@@ -2613,3 +2613,18 @@ body[data-color="off"] .atom-label {
overflow-wrap: anywhere;
font-size: 11px;
}
+
+/* The silicon world keeps the original brain scenes one click away. */
+#scene-navigation .silicon-link {
+ color: #c5e0e1;
+ font-size: 11px;
+ text-decoration: none;
+ white-space: nowrap;
+ padding: 9px 4px;
+}
+@media (max-width: 820px) {
+ #scene-navigation .silicon-link {
+ font-size: 10px;
+ padding: 9px 3px;
+ }
+}
diff --git a/tests/silicon-browser.mjs b/tests/silicon-browser.mjs
new file mode 100644
index 0000000..74148f9
--- /dev/null
+++ b/tests/silicon-browser.mjs
@@ -0,0 +1,482 @@
+import { chromium } from "playwright";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+
+const base = process.env.DOKE_URL || "http://127.0.0.1:4181";
+const isMac = process.platform === "darwin";
+const outputDir = "test-results/silicon";
+fs.mkdirSync(outputDir, { recursive: true });
+const checks = [];
+const errors = [];
+const requests = [];
+const appUrl = new URL(base);
+if (!appUrl.pathname.endsWith("/silicon.html")) {
+ appUrl.pathname = appUrl.pathname.replace(/\/$/, "") + "/silicon.html";
+}
+const backend = process.env.DOKE_BACKEND || (isMac ? "webgpu" : "webgl");
+appUrl.searchParams.set("backend", backend);
+
+const browser = await chromium.launch({
+ executablePath:
+ process.env.CHROME_PATH ||
+ (isMac
+ ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+ : undefined),
+ headless: true,
+ args: isMac
+ ? ["--enable-unsafe-webgpu", "--use-angle=metal"]
+ : ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"],
+});
+
+function observe(page) {
+ page.on("pageerror", (error) => errors.push(error.message));
+ page.on("response", (response) => {
+ if (response.status() >= 400) {
+ requests.push(`${response.status()} ${response.url()}`);
+ }
+ });
+ page.on("requestfailed", (request) => {
+ requests.push(`${request.failure()?.errorText} ${request.url()}`);
+ });
+}
+
+async function ready(page, url = appUrl.href) {
+ observe(page);
+ await page.goto(url);
+ await page.waitForFunction(
+ () =>
+ document.documentElement.dataset.ready === "true" &&
+ typeof window.dokeSilicon?.snapshot === "function" &&
+ typeof window.dokeSilicon?.finish === "function",
+ null,
+ { timeout: 90000 },
+ );
+ assert.equal(await page.title(), "doke");
+ const requestedBackend = new URL(url).searchParams.get("backend");
+ assert.match(
+ await page.locator("#render-status").textContent(),
+ requestedBackend === "webgl" ? /\bGl\b/ : /BrowserWebGpu/,
+ `The requested 3D renderer initialized; numerical fallback alone is insufficient. Renderer details: ${await page.locator("#loading").textContent()}. Page errors: ${errors.join("; ")}`,
+ );
+}
+
+const snapshot = (page) => page.evaluate(() => window.dokeSilicon.snapshot());
+
+async function finish(page) {
+ await page.evaluate(async () => {
+ await window.dokeSilicon.finish();
+ });
+ return snapshot(page);
+}
+
+function multiply(a, b) {
+ assert.equal(a.length, 64, "8×8 input matrix");
+ assert.equal(b.length, 64, "8×8 filter matrix");
+ const output = new Array(64).fill(0);
+ for (let row = 0; row < 8; row++) {
+ for (let col = 0; col < 8; col++) {
+ for (let k = 0; k < 8; k++) {
+ output[row * 8 + col] += a[row * 8 + k] * b[k * 8 + col];
+ }
+ }
+ }
+ return output;
+}
+
+function assertComplete(state, description) {
+ assert.equal(state.status, "complete", description);
+ assert.equal(state.valid.length, 64);
+ assert.ok(state.valid.every(Boolean), "All output values have arrived");
+ assert.equal(state.counters.completed_outputs, 64);
+ assert.deepEqual(
+ state.output,
+ multiply(state.input, state.weights),
+ description,
+ );
+}
+
+function assertConserved(state) {
+ assert.equal(state.conserved, true);
+ assert.equal(
+ state.attempted,
+ state.dropped + state.delivered + state.occupied,
+ );
+ assert.equal(state.admitted, state.delivered + state.occupied);
+ assert.equal(state.occupied, state.buffered + state.in_flight);
+ assert.ok(state.occupied <= state.capacity_packets);
+ assert.equal(state.packets.length, state.occupied);
+ assert.equal(
+ new Set(state.packets.map((packet) => packet.id)).size,
+ state.occupied,
+ );
+}
+
+async function screenshot(page, name) {
+ await page.screenshot({ path: path.join(outputDir, `${name}.png`) });
+}
+
+async function assertNoOverflow(page) {
+ assert.equal(
+ await page.evaluate(
+ () => document.documentElement.scrollWidth <= innerWidth + 1,
+ ),
+ true,
+ "No horizontal document overflow",
+ );
+}
+
+async function inspectBlock(page, key) {
+ const target = await page.evaluate(async (blockKey) => {
+ const { makeLayout } = await import("./silicon-layouts.js");
+ const block = makeLayout(window.dokeSilicon.family()).blocks.find(
+ (item) => item.key === blockKey,
+ );
+ if (!block) throw new Error(`Unknown block: ${blockKey}`);
+ const canvas = document.querySelector("#chip-canvas");
+ const rect = canvas.getBoundingClientRect();
+ const point = window.dokeSilicon
+ .getRenderer()
+ .project(
+ block.position[0],
+ block.position[1] + block.size[1] / 2,
+ block.position[2],
+ );
+ return {
+ x: rect.x + (point[0] / canvas.width) * rect.width,
+ y: rect.y + (point[1] / canvas.height) * rect.height,
+ depth: point[2],
+ title: block.title,
+ };
+ }, key);
+ assert.ok(target.depth > 0 && target.depth < 1);
+ // Exercise the visible schematic's real hit test, not a test-only panel API.
+ await page.mouse.click(target.x, target.y);
+ await page
+ .locator(`[data-panel="block:${key}"][aria-selected="true"]`)
+ .waitFor();
+ assert.equal(
+ await page.locator("#inspection h2").textContent(),
+ target.title,
+ );
+ return target.title;
+}
+
+let page;
+try {
+ page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
+ await ready(page);
+ await page.locator('[data-family="cpu"]').click();
+ await page.locator("#reset").click();
+
+ const profiles = await page.evaluate(async () => {
+ const response = await fetch(new URL("hardware.json", location.href));
+ if (!response.ok) throw new Error("Hardware catalog could not be loaded");
+ return (await response.json()).profiles;
+ });
+ const profileIds = profiles.map((profile) => profile.id);
+ const offeredIds = await page
+ .locator("#chip-profile option")
+ .evaluateAll((options) => options.map((option) => option.value));
+ for (const id of offeredIds) assert.ok(profileIds.includes(id), id);
+ assert.ok(
+ offeredIds.length > 0,
+ "The catalog provides selectable references",
+ );
+ const cpu = profiles.find((profile) => profile.family === "cpu");
+ await page.locator("#chip-profile").selectOption(cpu.id);
+ checks.push("Catalog-backed profile selection");
+
+ const beforeStep = await snapshot(page);
+ await page.locator("#step").click();
+ assert.equal((await snapshot(page)).tick, beforeStep.tick + 1);
+ await page.locator("#reset").click();
+ const reset = await snapshot(page);
+ assert.equal(reset.tick, 0);
+ assert.equal(reset.counters.completed_outputs, 0);
+ assert.ok(reset.valid.every((value) => !value));
+ await page.locator("#run").click();
+ await page.waitForFunction(() => window.dokeSilicon.snapshot().tick > 0);
+ await page.locator("#run").click();
+ const paused = await snapshot(page);
+ await page.waitForTimeout(300);
+ assert.deepEqual(await snapshot(page), paused, "Pause freezes model state");
+ checks.push(
+ "Pause freezes state; one step advances one tick; reset clears output",
+ );
+
+ await page.locator("#reset").click();
+ const firstBlockTitle = await inspectBlock(page, "scheduler");
+ await page.locator("#close-inspector").click();
+ await inspectBlock(page, "compute");
+ await page.locator('[data-panel="block:scheduler"]').click();
+ for (let i = 0; i < 6; i++) {
+ await page.locator("#step").click();
+ const node = (await snapshot(page)).nodes.find(
+ (item) => item.id === "scheduler",
+ );
+ assert.ok(node);
+ assert.equal(
+ await page.locator("#inspection h2").textContent(),
+ firstBlockTitle,
+ "Stepping refreshes the active tab, not the most recently picked block",
+ );
+ assert.deepEqual(await page.locator("#inspection dd").allTextContents(), [
+ node.state,
+ `${node.occupancy} / ${node.capacity}`,
+ ]);
+ }
+ await screenshot(page, "block-tabs");
+ await page.locator("#close-inspector").click();
+ checks.push(
+ "Returning between block tabs preserves identity and live model values",
+ );
+
+ await page.locator("#reset").click();
+ await page.locator("#input-preset").selectOption("stripes");
+ const inputBefore = (await snapshot(page)).input;
+ const tile = page.locator("#input-tile");
+ const tileBox = await tile.boundingBox();
+ assert.ok(tileBox);
+ await tile.click({
+ position: { x: tileBox.width / 16, y: tileBox.height / 16 },
+ });
+ const drawnInput = (await snapshot(page)).input;
+ assert.notDeepEqual(
+ drawnInput,
+ inputBefore,
+ "Drawing changes numerical input",
+ );
+ await tile.focus();
+ await tile.press("Space");
+ assert.notDeepEqual(
+ (await snapshot(page)).input,
+ drawnInput,
+ "Keyboard input changes a matrix cell",
+ );
+ checks.push("Pointer and keyboard editing change the actual input matrix");
+
+ const outputs = [];
+ for (const family of ["cpu", "gpu", "tensor"]) {
+ await page.locator(`[data-family="${family}"]`).click();
+ await page.locator("#input-preset").selectOption("diagonal");
+ const before = await snapshot(page);
+ assert.equal(before.mode, family);
+ assert.equal(before.tick, 0, "Changing input begins a fresh run");
+ const complete = await finish(page);
+ assertComplete(
+ complete,
+ `${family} matches independent matrix multiplication`,
+ );
+ outputs.push({
+ input: complete.input,
+ weights: complete.weights,
+ output: complete.output,
+ });
+ await screenshot(page, family);
+ }
+ assert.deepEqual(outputs[0], outputs[1]);
+ assert.deepEqual(outputs[1], outputs[2]);
+ checks.push("CPU/GPU/tensor output agrees with independent C=A×B reference");
+
+ await page.locator('[data-family="gpu"]').click();
+ await page.locator("#input-preset").selectOption("checker");
+ await page.locator("#memory-layout").selectOption("packed");
+ const packed = await finish(page);
+ await page.locator("#memory-layout").selectOption("strided");
+ const strided = await finish(page);
+ assertComplete(packed, "Packed GPU reference");
+ assertComplete(strided, "Strided GPU reference");
+ assert.deepEqual(strided.input, packed.input);
+ assert.deepEqual(strided.output, packed.output);
+ assert.ok(
+ strided.counters.sectors > packed.counters.sectors,
+ "Scattered addresses request more sectors without changing the answer",
+ );
+ checks.push("Layout changes traffic while preserving the numerical result");
+
+ await page.locator("#reference").click();
+ assert.equal(await page.locator("#inspector").isVisible(), true);
+ const referenceTab = page.locator(
+ '#inspector [data-panel][aria-selected="true"]',
+ );
+ const referenceKey = await referenceTab.getAttribute("data-panel");
+ assert.ok(referenceKey, "Reference has a selected tab");
+ await page.locator("#inspector [data-source]").first().click();
+ const tabs = await page
+ .locator("#inspector [data-panel]")
+ .evaluateAll((items) => items.map((item) => item.dataset.panel));
+ assert.ok(
+ tabs.includes(referenceKey),
+ "Source reading retains the reference tab",
+ );
+ assert.ok(tabs.length >= 2, "Source opens an additional tab");
+ const external = page.locator('#inspector a[href^="https://"]');
+ assert.ok(await external.count(), "Source links to its primary reference");
+ await page.locator(`#inspector [data-panel="${referenceKey}"]`).click();
+ assert.equal(
+ await page
+ .locator(`#inspector [data-panel="${referenceKey}"]`)
+ .getAttribute("aria-selected"),
+ "true",
+ );
+ await screenshot(page, "source-tabs");
+ // A fresh page keeps panel dismissal independent of the inspector's design.
+ await page.close();
+ page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
+ await ready(page);
+ checks.push("Source tabs preserve and restore the reference context");
+
+ await page.locator('[data-family="switch"]').click();
+ await page.locator("#queue-mode").selectOption("fifo");
+ await page.locator("#reset").click();
+ await page.locator("#block-0").click();
+ await page.locator("#inject-0").click();
+ await page.locator("#inject-1").click();
+ let fabric = await snapshot(page);
+ assertConserved(fabric);
+ assert.equal(fabric.blocked[0], true);
+ assert.equal(fabric.config.queue_mode, "fifo");
+ for (let i = 0; i < 8; i++) await page.locator("#step").click();
+ fabric = await snapshot(page);
+ assertConserved(fabric);
+ assert.equal(fabric.delivered_by_egress[0], 0);
+ assert.equal(
+ fabric.delivered_by_egress[1],
+ 0,
+ "FIFO head blocks following traffic",
+ );
+ assert.ok(fabric.occupied > 0);
+ const admittedBeforeSwitch = fabric.admitted;
+ await page.locator("#queue-mode").selectOption("voq");
+ for (let i = 0; i < 8; i++) await page.locator("#step").click();
+ fabric = await snapshot(page);
+ assertConserved(fabric);
+ assert.equal(
+ fabric.admitted,
+ admittedBeforeSwitch,
+ "Mode change retains packets",
+ );
+ assert.equal(fabric.delivered_by_egress[0], 0);
+ assert.ok(
+ fabric.delivered_by_egress[1] > 0,
+ "VOQ releases unblocked traffic",
+ );
+ await screenshot(page, "switch-blocked");
+ await page.locator("#block-0").click();
+ for (let i = 0; i < 40 && (await snapshot(page)).occupied; i++) {
+ await page.locator("#step").click();
+ }
+ fabric = await snapshot(page);
+ assertConserved(fabric);
+ assert.equal(
+ fabric.occupied,
+ 0,
+ "Releasing the egress drains admitted traffic",
+ );
+ await page.locator("#reset").click();
+ fabric = await snapshot(page);
+ assertConserved(fabric);
+ assert.equal(fabric.attempted, 0);
+ assert.equal(fabric.tick, 0);
+ assert.deepEqual(fabric.blocked, [false, false, false]);
+ checks.push(
+ "Blocked FIFO, VOQ intervention, conservation, drain and switch reset",
+ );
+
+ const mobile = await browser.newPage({
+ viewport: { width: 390, height: 844 },
+ deviceScaleFactor: 2,
+ isMobile: true,
+ hasTouch: true,
+ reducedMotion: "reduce",
+ });
+ await ready(mobile);
+ const quiet = await snapshot(mobile);
+ await mobile.waitForTimeout(300);
+ assert.deepEqual(
+ await snapshot(mobile),
+ quiet,
+ "Reduced motion starts paused",
+ );
+ await assertNoOverflow(mobile);
+ await mobile.locator('[data-family="cpu"]').tap();
+ await mobile.locator("#input-preset").selectOption("stripes");
+ const touchBefore = (await snapshot(mobile)).input;
+ const touchBox = await mobile.locator("#input-tile").boundingBox();
+ assert.ok(touchBox);
+ await mobile.touchscreen.tap(
+ touchBox.x + touchBox.width / 16,
+ touchBox.y + touchBox.height / 16,
+ );
+ assert.notDeepEqual((await snapshot(mobile)).input, touchBefore);
+ await mobile.locator("#step").tap();
+ assert.equal((await snapshot(mobile)).tick, 1);
+ await screenshot(mobile, "mobile-compute");
+ await mobile.locator('[data-family="switch"]').tap();
+ await mobile.locator("#inject-2").tap();
+ assertConserved(await snapshot(mobile));
+ await assertNoOverflow(mobile);
+ await screenshot(mobile, "mobile-switch");
+ await mobile.locator("#reference").tap();
+ const panelBox = await mobile.locator("#inspector").boundingBox();
+ assert.ok(panelBox && panelBox.x >= -1 && panelBox.x + panelBox.width <= 391);
+ await assertNoOverflow(mobile);
+ await screenshot(mobile, "mobile-reference");
+ await mobile.close();
+ checks.push(
+ "390×844 touch input, controls, inspector bounds and reduced motion",
+ );
+
+ if (backend !== "webgl") {
+ const gl = await browser.newPage({
+ viewport: { width: 1280, height: 900 },
+ });
+ const glUrl = new URL(appUrl);
+ glUrl.searchParams.set("backend", "webgl");
+ await ready(gl, glUrl.href);
+ await gl.locator('[data-family="tensor"]').click();
+ assertComplete(await finish(gl), "WebGL tensor result");
+ await screenshot(gl, "webgl");
+ await gl.close();
+ checks.push("WebGL fallback loads and preserves numerical output");
+ }
+
+ assert.deepEqual(errors, [], "No uncaught browser errors");
+ assert.deepEqual(requests, [], "All requested assets loaded successfully");
+ const report = {
+ status: "passed",
+ url: appUrl.href,
+ backend,
+ checks,
+ errors,
+ requests,
+ };
+ fs.writeFileSync(
+ path.join(outputDir, "results.json"),
+ JSON.stringify(report, null, 2),
+ );
+ console.log(JSON.stringify(report, null, 2));
+} catch (error) {
+ if (page && !page.isClosed())
+ await screenshot(page, "failure").catch(() => {});
+ fs.writeFileSync(
+ path.join(outputDir, "results.json"),
+ JSON.stringify(
+ {
+ status: "failed",
+ url: appUrl.href,
+ backend,
+ checks,
+ errors,
+ requests,
+ failure: String(error),
+ },
+ null,
+ 2,
+ ),
+ );
+ throw error;
+} finally {
+ await browser.close();
+}