This is an automated email from the ASF dual-hosted git repository.
spectrometerHBH pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-site.git
The following commit(s) were added to refs/heads/main by this push:
new 49dd10ba0b7 Self-host TIRx layout demo so the embedded iframe loads
under the site CSP (#61)
49dd10ba0b7 is described below
commit 49dd10ba0b7dbe4bb297b12f048b744bc5fa26c7
Author: Bohan Hou <[email protected]>
AuthorDate: Sun Jun 21 20:50:06 2026 -0700
Self-host TIRx layout demo so the embedded iframe loads under the site CSP
(#61)
---
_posts/2026-06-22-tirx.md | 3 +-
assets/tirx-layout-demo/index.html | 167 +++++++
assets/tirx-layout-demo/layout-demo.js | 805 +++++++++++++++++++++++++++++++++
assets/tirx-layout-demo/viz-base.css | 113 +++++
assets/tirx-layout-demo/viz-base.js | 72 +++
5 files changed, 1158 insertions(+), 2 deletions(-)
diff --git a/_posts/2026-06-22-tirx.md b/_posts/2026-06-22-tirx.md
index 902c19d9074..4407eac09d8 100644
--- a/_posts/2026-06-22-tirx.md
+++ b/_posts/2026-06-22-tirx.md
@@ -119,11 +119,10 @@ For example, element 57 at logical (3, 9) maps to:
<details>
<summary>Unfold to see the interactive layout demo</summary>
-<iframe id="tirx-layout-demo"
data-src="https://mlc.ai/modern-gpu-programming-for-mlsys/_static/tirx-layout-demo/index.html?preset=tensor-core&notitle&lock"
+<iframe id="tirx-layout-demo"
src="/assets/tirx-layout-demo/index.html?preset=tensor-core&notitle&lock"
style="width:100%; height:560px; border:1px solid #dfe1e6;
border-radius:10px; margin:12px 0;"
title="TIRx interactive layout demo: tensor-core tile"
loading="lazy"></iframe>
<script>
-(function () { var f = document.getElementById('tirx-layout-demo'); if (f &&
!f.src) f.src = f.getAttribute('data-src'); })();
window.addEventListener('message', function (e) {
var h = e.data && e.data.tirxLayoutDemoHeight;
if (!h) return;
diff --git a/assets/tirx-layout-demo/index.html
b/assets/tirx-layout-demo/index.html
new file mode 100644
index 00000000000..38d5c05ce20
--- /dev/null
+++ b/assets/tirx-layout-demo/index.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+ Page structure and interaction pattern adapted from the team's own course
+ material (mlsyscourse/slides-modern-gpu-programming,
+ data-layout/site/demo/tile_distributed.html). TIRx layout logic in
+ layout-demo.js is original. Not derived from any third-party demo.
+-->
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>TIRx Tensor Layout — Interactive Demo</title>
+<link rel="stylesheet" href="viz-base.css">
+<script src="viz-base.js"></script>
+<style>
+ /* embed mode (?notitle / ?lock) */
+ body.lock .preset-row, body.lock .main-controls, body.lock #status {
display:none; }
+ body.notitle, body.lock { padding:10px; }
+ /* lock: lay panels at natural size; JS scales the whole block to fit the
width */
+ body.lock #panels { display:flex; flex-direction:column;
align-items:flex-start; gap:20px; grid-template-columns:none;
width:max-content; max-width:none; margin:0; }
+
+ /* Demo-specific overrides */
+ .ti {
+ padding:5px 9px; border:1px solid var(--border); border-radius:5px;
+ font-family:'SF Mono','Fira Code',monospace; font-size:13px;
color:var(--text);
+ background:var(--surface);
+ }
+ .ti.expr { min-width:440px; max-width:70vw; }
+ .ti.shape { width:84px; }
+ select.ti { font-family:inherit; cursor:pointer; }
+
+ /* preset = secondary helper (just loads an example into the two fields) */
+ .preset-row { display:flex; align-items:center; gap:8px;
justify-content:center;
+ margin-bottom:8px; font-size:12px; }
+ .preset-row .lbl { font-size:12px; }
+ .preset-hint { color:var(--dim); font-size:12px; font-style:italic; }
+
+ /* shape + layout = the first-class inputs */
+ .main-controls { display:flex; gap:20px; justify-content:center;
align-items:flex-end;
+ flex-wrap:wrap; padding:16px 20px; border:1px solid #c7d6f0;
border-radius:10px;
+ background:#f6f9ff; max-width:960px; margin:0 auto 10px; }
+ .field { display:flex; flex-direction:column; gap:5px; }
+ .field.grow { flex:1; min-width:300px; }
+ .flbl { font-size:13px; font-weight:700; color:var(--text); }
+ .flbl-sub { font-weight:500; color:var(--dim); font-size:11px;
+ font-family:'SF Mono','Fira Code',monospace; }
+ .field .ti { font-size:14px; padding:8px 11px; }
+ .field .ti.expr { width:100%; min-width:0; max-width:none; }
+ .sw-group { display:flex; gap:6px; }
+ .sw-group .ti { font-size:13px; padding:8px 8px; }
+
+ #status { font-size:12px; font-family:'SF Mono','Fira Code',monospace;
color:var(--dim);
+ text-align:center; margin-bottom:14px; }
+
+ /* Stack the two panels vertically (logical on top, physical below) */
+ .panels { grid-template-columns:1fr; max-width:1280px; align-items:start; }
+ .grid { gap:2px; }
+ .cell { aspect-ratio:1; font-size:21px; border-radius:5px; border-width:2px;
}
+ .cell.dm { opacity:.18; }
+
+ /* Right panel: physical-coordinate view (2D table or 1D wrapped tiles) */
+ #phys { max-height:580px; overflow:auto; padding:2px; }
+ .phys-table { display:grid; gap:3px; }
+ .ax-hdr { font-size:18px; color:var(--dim); font-weight:600;
+ font-family:'SF Mono','Fira Code',monospace; display:flex;
align-items:center; justify-content:center; }
+ .ax-hdr.row { justify-content:flex-end; padding-right:4px;
white-space:nowrap; }
+ .pcell { border:1px solid var(--border); border-radius:4px; min-height:30px;
padding:2px;
+ display:flex; flex-wrap:wrap; gap:2px; align-content:flex-start;
justify-content:center; transition:all .15s; }
+ .pcell.hov-cell { border-color:#222; box-shadow:0 0 7px rgba(59,130,246,.4);
}
+ .pcell.dm-cell { opacity:.16; }
+ /* bank mode: pack the elements of one bank word side by side (same size as
+ the logical-tensor cells), not stacked */
+ .phys-table.bank-mode .pcell { flex-wrap:nowrap; gap:2px; padding:2px; }
+
+ .phys-1d { display:flex; flex-wrap:wrap; gap:8px; }
+ .thread-tile { border:2px solid var(--border); border-radius:8px;
padding:5px 6px 6px;
+ background:var(--bg); min-width:54px; transition:all .15s; }
+ .thread-tile.hov-tile { box-shadow:0 0 8px rgba(59,130,246,.35);
border-color:#222; }
+ .thread-tile.dm-tile { opacity:.2; }
+ .thread-lbl { font-size:18px; font-weight:700; color:var(--dim);
+ font-family:'SF Mono','Fira Code',monospace; text-align:center;
margin-bottom:4px; white-space:nowrap; }
+ .thread-slots { display:flex; flex-wrap:wrap; gap:2px;
justify-content:center; }
+
+ .gcell {
+ border-radius:3px; display:flex; align-items:center;
justify-content:center;
+ font-size:21px; font-weight:700; width:46px; height:46px; flex:0 0 auto;
cursor:pointer;
+ border:2px solid transparent; transition:all .15s;
+ }
+ .gcell.hov { border-color:#222; box-shadow:0 0 6px rgba(59,130,246,.45); }
+ .gcell.dm { opacity:.18; }
+
+ .formula-bar { max-width:1280px; }
+ /* Larger labels/headers/legend for embedded (scaled) readability */
+ .panel h2 { font-size:22px; }
+ .panel .nota { font-size:19px; }
+ .hdr { font-size:18px; }
+ .rl { font-size:18px; }
+ .li { font-size:14px; }
+</style>
+</head>
+<body>
+
+<h1>TIRx Tensor Layout</h1>
+<div class="sub">how a <code>TileLayout</code> (shard / replica / offset) maps
logical tensor elements to physical threads</div>
+
+<div class="preset-row">
+ <span class="lbl">load an example</span>
+ <select id="preset" class="ti"></select>
+ <span class="preset-hint">↓ fills the two fields below</span>
+</div>
+
+<div class="main-controls">
+ <div class="field">
+ <label class="flbl">Logical shape</label>
+ <input id="shape" class="ti shape" value="4, 8" spellcheck="false">
+ </div>
+ <div class="field">
+ <label class="flbl">Swizzle (SMEM)</label>
+ <div class="sw-group">
+ <select id="dtype" class="ti"></select>
+ <select id="swmode" class="ti"></select>
+ </div>
+ </div>
+ <div class="field grow">
+ <label class="flbl">Tile layout <span class="flbl-sub">S[shard]
+ R[replica] + offset</span></label>
+ <input id="expr" class="ti expr" value="S[(4,8):(8@laneid,1@laneid)]"
spellcheck="false">
+ </div>
+</div>
+<div id="status"></div>
+
+<div class="panels" id="panels">
+ <div class="panel">
+ <h2>Logical tensor</h2>
+ <div class="nota" id="n0"></div>
+ <div class="grid" id="g0"></div>
+ </div>
+ <div class="panel">
+ <h2>Physical threads</h2>
+ <div class="nota" id="nphys"></div>
+ <div id="phys"></div>
+ </div>
+ <svg class="arrow-svg" id="arrow"></svg>
+</div>
+
+<div class="formula-bar" id="fb"></div>
+<div class="leg" id="lg"></div>
+
+<script src="layout-demo.js"></script>
+</body>
+</html>
diff --git a/assets/tirx-layout-demo/layout-demo.js
b/assets/tirx-layout-demo/layout-demo.js
new file mode 100644
index 00000000000..ec117accaf3
--- /dev/null
+++ b/assets/tirx-layout-demo/layout-demo.js
@@ -0,0 +1,805 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * TIRx tensor-layout visualizer.
+ *
+ * The page shell, CSS vocabulary, and the draw()/hover/arrow interaction
+ * pattern are adapted from the team's own course material
+ * (mlsyscourse/slides-modern-gpu-programming, data-layout/site/demo/
+ * tile_distributed.html). The TIRx S/R/O parser and the logical->physical
+ * mapper below are original and mirror tvm/python/tvm/tirx/layout.py
+ * (_flatten_coord / _split_coord and the TileLayout forward mapping).
+ * This file is NOT derived from any third-party layout demo.
+ */
+
+'use strict';
+
+// ── TIRx named axes (from tvm/python/tvm/tirx/layout.py _AXIS_NAMES, plus the
+// device axis `pid` used by distributed layouts)
──────────────────────────────
+const AXIS_ORDER = [
+ 'pid', 'bx', 'by', 'bz', 'cbx', 'cby', 'cbz', 'tx',
+ 'warpid', 'laneid', 'wgid', 'tid_in_wg', 'wid_in_wg', 'tid',
+ 'm', 'P', 'F', 'Bank', 'TCol', 'TLane',
+];
+// "Owner" axes name a physical unit that owns data (threads, devices); the
rest
+// (m, P, F, Bank, TCol, TLane) are storage/memory coordinates within an owner.
+const OWNER_AXES = new Set([
+ 'pid', 'bx', 'by', 'bz', 'cbx', 'cby', 'cbz', 'tx',
+ 'warpid', 'laneid', 'wgid', 'tid_in_wg', 'wid_in_wg', 'tid',
+]);
+const KNOWN_AXES = new Set(AXIS_ORDER);
+const MAX_ELEMENTS = 1024; // render cap
+
+function isOwnerAxis(a) { return OWNER_AXES.has(a); }
+function product(arr) { return arr.reduce((a, b) => a * b, 1); }
+
+// ── Parser
───────────────────────────────────────────────────────────────────
+// Grammar mirrors layout.py: S[shape:stride] + R[shape:stride] + offset,
+// stride/offset terms are "n@axis" (a bare int defaults to axis "m").
+
+function splitTopLevel(s, sep) {
+ const out = [];
+ let depth = 0, cur = '';
+ for (const ch of s) {
+ if (ch === '(' || ch === '[') depth++;
+ else if (ch === ')' || ch === ']') {
+ depth--;
+ if (depth < 0) throw new Error('unmatched closing bracket or
parenthesis');
+ }
+ if (ch === sep && depth === 0) { out.push(cur); cur = ''; }
+ else cur += ch;
+ }
+ if (depth !== 0) throw new Error('unmatched opening bracket or parenthesis');
+ out.push(cur);
+ return out;
+}
+
+function stripParens(s) {
+ s = s.trim();
+ if (s.startsWith('(') && s.endsWith(')')) return s.slice(1, -1);
+ return s;
+}
+
+function parseIntStrict(s) {
+ const t = s.trim();
+ if (!/^-?\d+$/.test(t)) throw new Error(`expected integer, got "${t}"`);
+ return parseInt(t, 10);
+}
+
+function parseTerm(tok) {
+ const t = tok.trim();
+ if (t.includes('@')) {
+ const parts = t.split('@');
+ if (parts.length !== 2) throw new Error(`bad term "${t}"`);
+ const numPart = parts[0].trim();
+ const axis = parts[1].trim();
+ if (!KNOWN_AXES.has(axis)) throw new Error(`unknown axis "${axis}"`);
+ const stride = numPart === '' ? 1 : parseIntStrict(numPart);
+ return { stride, axis };
+ }
+ if (/^-?\d+$/.test(t)) return { stride: parseInt(t, 10), axis: 'm' };
+ if (KNOWN_AXES.has(t)) return { stride: 1, axis: t };
+ throw new Error(`bad term "${t}"`);
+}
+
+function defaultStrides(extents) {
+ const n = extents.length;
+ const strides = new Array(n).fill(1);
+ for (let i = n - 2; i >= 0; i--) strides[i] = strides[i + 1] * extents[i +
1];
+ return strides;
+}
+
+function parseExtents(s) {
+ // Parse a comma-separated extent list and reject non-positive / oversized
+ // extents, so bad input (e.g. R[1000000:...] or a 0/negative extent) can't
+ // NaN-propagate or freeze the tab with huge loops in physOwners().
+ const extents = splitTopLevel(stripParens(s), ',').map(parseIntStrict);
+ for (const e of extents) {
+ if (e <= 0) throw new Error(`extent must be positive, got ${e}`);
+ if (e > MAX_ELEMENTS) throw new Error(`extent ${e} exceeds maximum
${MAX_ELEMENTS}`);
+ }
+ return extents;
+}
+
+function parseBracket(inner) {
+ const parts = splitTopLevel(inner, ':');
+ if (parts.length === 1) {
+ const extents = parseExtents(parts[0]);
+ const strides = defaultStrides(extents);
+ return extents.map((e, i) => ({ extent: e, stride: strides[i], axis: 'm'
}));
+ }
+ if (parts.length !== 2) throw new Error('layout bracket must be "shape :
stride"');
+ const extents = parseExtents(parts[0]);
+ const terms = splitTopLevel(stripParens(parts[1]), ',').map(parseTerm);
+ if (extents.length !== terms.length) {
+ throw new Error(`shape has ${extents.length} dims but stride has
${terms.length}`);
+ }
+ return extents.map((e, i) => ({ extent: e, stride: terms[i].stride, axis:
terms[i].axis }));
+}
+
+function bracketBody(piece, prefix) {
+ const open = piece.indexOf('[');
+ const close = piece.lastIndexOf(']');
+ if (open < 0 || close < 0 || close < open) throw new Error(`malformed
${prefix}[...]`);
+ return piece.slice(open + 1, close);
+}
+
+function parseSwizzlePrefix(src) {
+ // Optional "Swizzle(per_element, swizzle_len, atom_len[, inner]) [∘|o|*]
<layout>".
+ const m = src.match(/^Swizzle\s*\(([^)]*)\)\s*(?:∘|o|\.|\*)?\s*([\s\S]*)$/i);
+ if (!m) return { swizzle: null, rest: src };
+ const a = m[1].split(',').map((s) => s.trim()).filter((s) => s.length);
+ if (a.length < 3) throw new Error('Swizzle needs (per_element, swizzle_len,
atom_len)');
+ const per_element = parseIntStrict(a[0]);
+ const swizzle_len = parseIntStrict(a[1]);
+ const atom_len = parseIntStrict(a[2]);
+ if (per_element < 0 || swizzle_len < 0 || atom_len < swizzle_len
+ || per_element >= 31 || atom_len >= 31) {
+ // atom_len/per_element feed 32-bit bitwise shifts in swizzleAddr; cap <
31.
+ throw new Error('swizzle requires 0≤per_element<31, swizzle_len≥0,
swizzle_len≤atom_len<31');
+ }
+ const inner = a[3] === undefined ? true : (a[3] === 'true' || a[3] === '1');
+ return { swizzle: { per_element, swizzle_len, atom_len, inner }, rest:
m[2].trim() };
+}
+
+function parseLayout(srcRaw) {
+ const { swizzle, rest } = parseSwizzlePrefix(srcRaw.trim());
+ const src = rest;
+ const layout = { shard: [], replica: [], offset: {}, swizzle };
+ let sawShard = false;
+ for (let piece of splitTopLevel(src, '+')) {
+ piece = piece.trim();
+ if (piece === '') continue;
+ if (piece.startsWith('S[')) {
+ layout.shard = parseBracket(bracketBody(piece, 'S'));
+ sawShard = true;
+ } else if (piece.startsWith('R[')) {
+ layout.replica = parseBracket(bracketBody(piece, 'R'));
+ } else {
+ const t = parseTerm(piece);
+ layout.offset[t.axis] = (layout.offset[t.axis] || 0) + t.stride;
+ }
+ }
+ if (!sawShard) throw new Error('layout needs a shard term, e.g. S[...]');
+ return layout;
+}
+
+// ── Mapper (mirrors layout.py _flatten_coord / _split_coord + forward map)
─────
+
+function flattenCoord(coord, shape) {
+ let flat = 0;
+ for (let i = 0; i < shape.length; i++) flat = flat * shape[i] + coord[i];
+ return flat;
+}
+
+function splitCoord(flat, extents) {
+ const n = extents.length;
+ const res = new Array(n);
+ let remaining = flat;
+ for (let i = n - 1; i >= 0; i--) {
+ if (i === 0) res[0] = remaining;
+ else { res[i] = remaining % extents[i]; remaining = Math.floor(remaining /
extents[i]); }
+ }
+ return res;
+}
+
+function coordFromFlat(flat, shape) { return splitCoord(flat, shape); }
+
+function forwardBase(coord, shape, layout) {
+ const flat = flattenCoord(coord, shape);
+ const comps = splitCoord(flat, layout.shard.map((it) => it.extent));
+ const phys = {};
+ for (let k = 0; k < layout.shard.length; k++) {
+ const it = layout.shard[k];
+ phys[it.axis] = (phys[it.axis] || 0) + comps[k] * it.stride;
+ }
+ for (const axis of Object.keys(layout.offset)) {
+ phys[axis] = (phys[axis] || 0) + layout.offset[axis];
+ }
+ return phys;
+}
+
+// Replica broadcasts the same logical element onto multiple physical owners:
+// L(x) = { D(x) + r + O | r in R }.
+function physOwners(coord, shape, layout) {
+ let owners = [forwardBase(coord, shape, layout)];
+ for (const rep of layout.replica) {
+ const next = [];
+ for (const o of owners) {
+ for (let k = 0; k < rep.extent; k++) {
+ const o2 = Object.assign({}, o);
+ o2[rep.axis] = (o2[rep.axis] || 0) + k * rep.stride;
+ next.push(o2);
+ }
+ }
+ owners = next;
+ }
+ return owners;
+}
+
+function axesUsed(layout) {
+ const s = new Set();
+ for (const it of layout.shard) s.add(it.axis);
+ for (const it of layout.replica) s.add(it.axis);
+ for (const a of Object.keys(layout.offset)) s.add(a);
+ return AXIS_ORDER.filter((a) => s.has(a));
+}
+
+function coordStr(phys, axes) {
+ return axes.map((a) => `${a}=${phys[a] || 0}`).join(' ');
+}
+
+// Swizzle a linear memory address (mirrors
src/tirx/ir/layout/swizzle_layout.cc
+// SwizzleLayoutNode::Apply): low `per_element` bits are kept; above them, the
+// swizzle bits are XOR'd to scatter bank conflicts.
+function swizzleAddr(m, sw) {
+ const base = 1 << sw.per_element;
+ const innerMask = (1 << sw.swizzle_len) - 1;
+ const outerMask = innerMask << sw.atom_len;
+ const x = Math.floor(m / base);
+ const fx = sw.inner ? (x ^ ((x & outerMask) >> sw.atom_len))
+ : (x ^ ((x & innerMask) << sw.atom_len));
+ return fx * base + (m % base);
+}
+
+// Resolve the swizzle from the dtype + mode dropdowns (mirrors
+// tma_utils.mma_atom_layout): per_element = bit_length(128//bits) - 1,
+// swizzle_len = mode, atom_len = 3. Falls back to a typed Swizzle(...) prefix.
+const SWIZZLE_LEN = { none: 0, '32': 1, '64': 2, '128': 3 };
+function computeSwizzle() {
+ const mode = swmodeSel ? swmodeSel.value : 'off';
+ if (mode === 'off') return (ST.layout && ST.layout.swizzle) ?
ST.layout.swizzle : null;
+ const bits = +(dtypeSel ? dtypeSel.value : 16) || 16;
+ const per_element = Math.floor(128 / bits).toString(2).length - 1;
+ return {
+ per_element, swizzle_len: SWIZZLE_LEN[mode] || 0, atom_len: 3, inner: true,
+ bits, mode,
+ };
+}
+
+// ── State + recompute
──────────────────────────────────────────────────────--
+function getComputedStyleVar(name) {
+ return
getComputedStyle(document.documentElement).getPropertyValue(name).trim();
+}
+const PALETTE = Array.from({ length: 8 }, (_, i) =>
+ getComputedStyleVar(`--color-group-${i}`) || '#5b9bd5');
+function paletteColor(v) { const n = PALETTE.length; return PALETTE[((v % n) +
n) % n]; }
+
+const ST = {
+ shape: [4, 8],
+ layout: null,
+ error: null,
+ tooBig: false,
+ banks: 32,
+ swizzle: null,
+ gridAxes: [], yAxis: null, xAxis: null, cellAxes: [],
+ yVals: [], xVals: [],
+ byFlat: [], // flat -> { owners:[phys], keys:[gridKey], color }
+ byCell: new Map(), // "y#x" -> [{flat, slot}]
+};
+let hovFlat = null;
+let drawing = false;
+
+function mk(t, c) { const d = document.createElement(t); if (c) d.className =
c; return d; }
+function gridKey(phys, axes) { return axes.map((a) => phys[a] || 0).join(',');
}
+
+function recompute() {
+ ST.error = null; ST.tooBig = false;
+ try {
+ ST.shape = splitTopLevel(stripParens(shapeInput.value),
',').map(parseIntStrict);
+ if (ST.shape.length === 0 || ST.shape.some((x) => x <= 0)) throw new
Error('shape must be positive ints');
+ ST.layout = parseLayout(exprInput.value);
+ } catch (e) { ST.error = e.message; return; }
+
+ const total = product(ST.shape);
+ if (total > MAX_ELEMENTS) { ST.tooBig = true; return; }
+
+ ST.shapeTotal = total;
+ ST.shardTotal = product(ST.layout.shard.map((it) => it.extent));
+ ST.mismatch = ST.shardTotal !== total;
+ ST.swizzle = computeSwizzle();
+ // elements that share one 4-byte bank word (e.g. 2 fp16, 4 fp8, 1 fp32)
+ ST.elemsPerBank = ST.swizzle ? Math.max(1, Math.round(4 / ((ST.swizzle.bits
|| 32) / 8))) : 1;
+
+ // 1) owners per element; in swizzle mode, also map the memory address
through
+ // the swizzle and derive synthetic line/bank coordinates.
+ ST.byFlat = new Array(total);
+ for (let flat = 0; flat < total; flat++) {
+ const owners = physOwners(coordFromFlat(flat, ST.shape), ST.shape,
ST.layout);
+ if (ST.swizzle) {
+ // A shared-memory bank is 4 bytes; an element occupies dtype_bytes, so
the
+ // bank word index is floor(element_addr * dtype_bytes / 4). 32 banks
per line.
+ const bytes = (ST.swizzle.bits || 32) / 8;
+ for (const o of owners) {
+ const sm = swizzleAddr(o.m || 0, ST.swizzle);
+ const word = Math.floor((sm * bytes) / 4);
+ o.__sm = sm; o.__word = word; o.bank = word % ST.banks; o.line =
Math.floor(word / ST.banks);
+ }
+ }
+ ST.byFlat[flat] = { owners };
+ }
+
+ // 2) choose grid + color axes
+ if (ST.swizzle) {
+ ST.gridAxes = ['line', 'bank']; ST.yAxis = 'line'; ST.xAxis = 'bank';
+ ST.cellAxes = []; ST.colorAxis = 'bank';
+ } else {
+ const used = axesUsed(ST.layout);
+ const owners = used.filter(isOwnerAxis);
+ ST.gridAxes = owners.length ? owners : used.filter((a) => !isOwnerAxis(a));
+ ST.yAxis = ST.gridAxes[0] || null;
+ ST.xAxis = ST.gridAxes[1] || null;
+ ST.cellAxes = used.filter((a) => !ST.gridAxes.includes(a));
+ // color axis = first grid axis from shard/offset, so a replica-only row
axis
+ // doesn't collapse every element to one color.
+ const shardOffsetAxes = new Set(ST.layout.shard.map((it) => it.axis));
+ for (const a of Object.keys(ST.layout.offset)) shardOffsetAxes.add(a);
+ ST.colorAxis = ST.gridAxes.find((a) => shardOffsetAxes.has(a)) ||
ST.gridAxes[0] || null;
+ }
+
+ // 3) build cells, hover keys, colors
+ ST.byCell = new Map();
+ const yset = new Set(), xset = new Set(), cset = new Set();
+ for (let flat = 0; flat < total; flat++) {
+ const rec = ST.byFlat[flat];
+ const keys = [];
+ for (const o of rec.owners) {
+ const y = ST.yAxis ? (o[ST.yAxis] || 0) : 0;
+ const x = ST.xAxis ? (o[ST.xAxis] || 0) : 0;
+ yset.add(y); xset.add(x);
+ keys.push(gridKey(o, ST.gridAxes));
+ const ck = y + '#' + x;
+ if (!ST.byCell.has(ck)) ST.byCell.set(ck, []);
+ ST.byCell.get(ck).push({ flat, slot: ST.swizzle ? ('addr ' + o.__sm) :
coordStr(o, ST.cellAxes) });
+ }
+ rec.keys = keys;
+ const cv = ST.colorAxis ? (rec.owners[0][ST.colorAxis] || 0) : 0;
+ rec.color = paletteColor(cv);
+ cset.add(cv);
+ }
+ ST.yVals = [...yset].sort((a, b) => a - b);
+ ST.xVals = [...xset].sort((a, b) => a - b);
+ ST.colorVals = [...cset].sort((a, b) => a - b);
+}
+
+// ── Display geometry for the logical grid
──────────────────────────────────--
+function logicalGridDims() {
+ if (ST.shape.length === 2) return { rows: ST.shape[0], cols: ST.shape[1] };
+ if (ST.shape.length === 1) return { rows: 1, cols: ST.shape[0] };
+ return { rows: 1, cols: product(ST.shape) }; // N-D -> flat strip
+}
+
+// ── Draw ────────────────────────────────────────────────────────────────────
+function resetFit() {
+ const p = document.getElementById('panels');
+ if (p) { p.style.transform = 'none'; p.style.marginBottom = ''; }
+}
+function fitEmbed() {
+ if (!document.body.classList.contains('lock')) return;
+ const p = document.getElementById('panels');
+ if (!p) return;
+ const natural = p.offsetWidth;
+ const pad = 2 * parseFloat(getComputedStyle(document.body).paddingLeft ||
'0');
+ const avail = document.documentElement.clientWidth - pad;
+ if (avail > 0 && natural > avail) {
+ const sc = avail / natural;
+ p.style.transformOrigin = 'top left';
+ p.style.transform = 'scale(' + sc + ')';
+ p.style.marginBottom = (-(p.offsetHeight * (1 - sc))) + 'px';
+ }
+}
+function postHeight() {
+ if (window.parent === window) return;
+ const h = Math.ceil(document.body.scrollHeight);
+ window.parent.postMessage({ tirxLayoutDemoHeight: h + 4 }, '*');
+}
+function draw() {
+ drawing = true;
+ resetFit();
+ const status = document.getElementById('status');
+ const g0 = document.getElementById('g0');
+ const phys = document.getElementById('phys');
+ const fb = document.getElementById('fb');
+ const lg = document.getElementById('lg');
+
+ if (ST.error) {
+ status.innerHTML = `<span style="color:var(--color-bad)">parse error:
${escapeHtml(ST.error)}</span>`;
+ g0.innerHTML = ''; phys.innerHTML = ''; lg.innerHTML = '';
+ fb.innerHTML = '<div class="ftitle">Fix the layout expression to
continue.</div>';
+ setTimeout(() => { drawing = false; }, 0); return;
+ }
+ if (ST.tooBig) {
+ status.innerHTML = `<span
style="color:var(--color-bad)">${product(ST.shape)} elements exceeds the
${MAX_ELEMENTS} render cap — use a smaller shape.</span>`;
+ g0.innerHTML = ''; phys.innerHTML = ''; lg.innerHTML = '';
+ fb.innerHTML = '<div class="ftitle">Shape too large to visualize.</div>';
+ setTimeout(() => { drawing = false; }, 0); return;
+ }
+
+ status.innerHTML = `<span style="color:var(--color-good)">ok</span> `
+
+ `${product(ST.shape)} logical elements | ` +
+ `${ST.yVals.length * (ST.xVals.length || 1)} physical cells`;
+ if (ST.mismatch) {
+ status.innerHTML += ` <span style="color:#b45309;font-weight:600">` +
+ `⚠ shard total ${ST.shardTotal} ≠ shape total ${ST.shapeTotal} — mapping
may be ill-formed</span>`;
+ }
+ if (ST.swizzle) {
+ const s = ST.swizzle;
+ const label = s.mode ? (s.mode === 'none' ? 'no swizzle' : s.mode + 'B
swizzle') : 'swizzle';
+ status.innerHTML += ` <span style="color:var(--dim)">` +
+ `${label}${s.bits ? ', ' + s.bits + '-bit' : ''} →
Swizzle(${s.per_element},${s.swizzle_len},${s.atom_len})</span>`;
+ }
+ document.getElementById('n0').textContent = `logical shape
(${ST.shape.join(', ')})`;
+ document.getElementById('nphys').textContent =
+ (ST.xAxis ? `rows = ${ST.yAxis}, cols = ${ST.xAxis}` : `tiles = ${ST.yAxis
|| '(none)'}`) +
+ (ST.cellAxes.length ? ' · in-cell: ' + ST.cellAxes.join(', ') : '');
+
+ const hovKeys = hovFlat !== null ? new Set(ST.byFlat[hovFlat].keys) : null;
+ drawLogical(hovKeys);
+ drawPhysical(hovKeys);
+ drawFormula();
+ drawArrow();
+ drawLegend();
+ fitEmbed();
+ postHeight();
+ setTimeout(() => { drawing = false; }, 0);
+}
+
+function sharesOwner(flat, hovKeys) {
+ if (!hovKeys) return false;
+ return ST.byFlat[flat].keys.some((k) => hovKeys.has(k));
+}
+
+function drawLogical(hovKeys) {
+ const g = document.getElementById('g0');
+ g.innerHTML = '';
+ const { rows, cols } = logicalGridDims();
+ g.style.gridTemplateColumns = '30px repeat(' + cols + ', 46px)';
+ g.appendChild(mk('div', 'hdr'));
+ for (let c = 0; c < cols; c++) { const h = mk('div', 'hdr'); h.textContent =
'c' + c; g.appendChild(h); }
+ for (let r = 0; r < rows; r++) {
+ const rl = mk('div', 'rl'); rl.textContent = (rows > 1) ? ('r' + r) : '';
g.appendChild(rl);
+ for (let c = 0; c < cols; c++) {
+ const flat = r * cols + c;
+ const d = mk('div', 'cell');
+ d.dataset.flat = flat;
+ d.textContent = flat;
+ d.style.background = ST.byFlat[flat].color;
+ d.style.color = '#fff';
+ if (hovFlat !== null) {
+ if (flat === hovFlat) d.classList.add('hov');
+ else if (!sharesOwner(flat, hovKeys)) d.classList.add('dm');
+ }
+ g.appendChild(d);
+ }
+ }
+}
+
+function cellEntries(y, x) { return ST.byCell.get(y + '#' + x) || []; }
+
+function makeSlot(entry) {
+ const s = mk('div', 'gcell');
+ s.style.background = ST.byFlat[entry.flat].color;
+ s.style.color = '#fff';
+ s.dataset.flat = entry.flat;
+ s.textContent = entry.flat;
+ s.title = entry.slot ? `element ${entry.flat} @ ${entry.slot}` : `element
${entry.flat}`;
+ if (hovFlat !== null) {
+ if (entry.flat === hovFlat) s.classList.add('hov');
+ else s.classList.add('dm');
+ }
+ return s;
+}
+
+function drawPhysical(hovKeys) {
+ const wrap = document.getElementById('phys');
+ wrap.innerHTML = '';
+ if (!ST.yAxis) { wrap.textContent = '(no physical axes)'; return; }
+
+ if (ST.xAxis) {
+ // 2D table: rows = yAxis values, cols = xAxis values.
+ const table = mk('div', 'phys-table' + (ST.swizzle ? ' bank-mode' : ''));
+ // In bank mode a cell is one 4-byte bank word holding elemsPerBank
elements
+ // laid out horizontally; otherwise a fixed 54px cell.
+ const colW = ST.swizzle ? (ST.elemsPerBank * 48 + 8) : 54;
+ table.style.gridTemplateColumns = '44px repeat(' + ST.xVals.length + ', '
+ colW + 'px)';
+ table.appendChild(corner());
+ for (const x of ST.xVals) table.appendChild(axHdr(String(x), false,
`${ST.xAxis}=${x}`));
+ for (const y of ST.yVals) {
+ table.appendChild(axHdr(String(y), true, `${ST.yAxis}=${y}`));
+ for (const x of ST.xVals) {
+ const cell = mk('div', 'pcell');
+ const entries = cellEntries(y, x);
+ if (hovFlat !== null && entries.some((e) => e.flat === hovFlat))
cell.classList.add('hov-cell');
+ else if (hovFlat !== null && entries.length)
cell.classList.add('dm-cell');
+ for (const e of entries) cell.appendChild(makeSlot(e));
+ table.appendChild(cell);
+ }
+ }
+ wrap.appendChild(table);
+ } else {
+ // 1D: wrapped list of owner tiles, one per yAxis value.
+ const list = mk('div', 'phys-1d');
+ for (const y of ST.yVals) {
+ const tile = mk('div', 'thread-tile');
+ const lbl = mk('div', 'thread-lbl'); lbl.textContent =
`${ST.yAxis}=${y}`; tile.appendChild(lbl);
+ const slots = mk('div', 'thread-slots');
+ const entries = cellEntries(y, 0).slice().sort((a, b) => a.flat -
b.flat);
+ if (hovFlat !== null && entries.some((e) => e.flat === hovFlat))
tile.classList.add('hov-tile');
+ else if (hovFlat !== null && entries.length)
tile.classList.add('dm-tile');
+ for (const e of entries) slots.appendChild(makeSlot(e));
+ tile.appendChild(slots);
+ list.appendChild(tile);
+ }
+ wrap.appendChild(list);
+ }
+}
+
+function corner() {
+ const d = mk('div', 'ax-hdr corner');
+ d.textContent = '↘';
+ if (ST.yAxis) d.title = `rows = ${ST.yAxis}` + (ST.xAxis ? `, cols =
${ST.xAxis}` : '');
+ return d;
+}
+function axHdr(text, isRow, title) {
+ const d = mk('div', 'ax-hdr' + (isRow ? ' row' : ''));
+ d.textContent = text;
+ if (title) d.title = title;
+ return d;
+}
+
+function drawFormula() {
+ const fb = document.getElementById('fb');
+ if (hovFlat === null) { fb.innerHTML = '<div class="ftitle">Click a logical
element to see its mapping.</div>'; return; }
+ const flat = hovFlat;
+ const coord = coordFromFlat(flat, ST.shape);
+ const comps = splitCoord(flat, ST.layout.shard.map((it) => it.extent));
+ const perAxis = {};
+ const termStrings = [];
+ for (let k = 0; k < ST.layout.shard.length; k++) {
+ const it = ST.layout.shard[k];
+ perAxis[it.axis] = (perAxis[it.axis] || 0) + comps[k] * it.stride;
+ termStrings.push(`${comps[k]}·${it.stride}@${it.axis}`);
+ }
+ const offStrings = [];
+ for (const axis of Object.keys(ST.layout.offset)) {
+ perAxis[axis] = (perAxis[axis] || 0) + ST.layout.offset[axis];
+ offStrings.push(`${ST.layout.offset[axis]}@${axis}`);
+ }
+ const owners = ST.byFlat[flat].owners;
+ let html = `<div class="ftitle">element ${flat} at logical (${coord.join(',
')})` +
+ ` → shard split = (${comps.join(', ')})</div>`;
+ html += '<div class="fcontent">';
+ html += `terms: ${termStrings.join(' + ')}` +
+ (offStrings.length ? ` + offset[${offStrings.join(', ')}]` : '') +
'<br>';
+ const baseParts = AXIS_ORDER.filter((a) => perAxis[a] !== undefined).map((a)
=> `<b>${perAxis[a]}</b>@${a}`);
+ html += `base location: ${baseParts.join(' , ')}`;
+ if (ST.swizzle) {
+ const o0 = owners[0];
+ const sw = ST.swizzle;
+ const bytes = (sw.bits || 32) / 8;
+ html += `<br>swizzle(${sw.per_element},${sw.swizzle_len},${sw.atom_len}):
` +
+ `m=${o0.m || 0} → elem ${o0.__sm} → byte ${o0.__sm * bytes} → ` +
+ `<b>bank ${o0.bank}</b>, line ${o0.line} (${bytes}-byte dtype, 4-byte
banks ×32)`;
+ }
+ if (owners.length > 1) {
+ html += `<br>owners (×${owners.length} via replica): ` +
+ owners.map((o) => '{ ' + coordStr(o, ST.gridAxes) + ' }').join(' , ');
+ }
+ html += '</div>';
+ fb.innerHTML = html;
+}
+
+// ── Arrow overlay (adapted from the course draw pattern)
───────────────────--
+const panels = document.getElementById('panels');
+const arrowSvg = document.getElementById('arrow');
+
+function drawArrow() {
+ arrowSvg.innerHTML = '';
+ if (hovFlat === null) return;
+ const leftCell = document.querySelector('#g0 .cell.hov');
+ const rightCells = document.querySelectorAll('#phys .gcell.hov');
+ if (!leftCell || rightCells.length === 0) return;
+ const pr = panels.getBoundingClientRect();
+ const ns = 'http://www.w3.org/2000/svg';
+ const defs = document.createElementNS(ns, 'defs');
+ const marker = document.createElementNS(ns, 'marker');
+ marker.setAttribute('id', 'ah'); marker.setAttribute('markerWidth', '8');
marker.setAttribute('markerHeight', '6');
+ marker.setAttribute('refX', '7'); marker.setAttribute('refY', '3');
marker.setAttribute('orient', 'auto');
+ const poly = document.createElementNS(ns, 'polygon');
+ poly.setAttribute('points', '0 0, 8 3, 0 6'); poly.setAttribute('fill',
'#222');
+ marker.appendChild(poly); defs.appendChild(marker);
arrowSvg.appendChild(defs);
+ const a = leftCell.getBoundingClientRect();
+ const x1 = a.left + a.width / 2 - pr.left, y1 = a.top + a.height / 2 -
pr.top;
+ rightCells.forEach((rc) => {
+ const b = rc.getBoundingClientRect();
+ const x2 = b.left + b.width / 2 - pr.left, y2 = b.top + b.height / 2 -
pr.top;
+ const mx = (x1 + x2) / 2, my = Math.min(y1, y2) - 24;
+ const path = document.createElementNS(ns, 'path');
+ path.setAttribute('d', `M${x1},${y1} Q${mx},${my} ${x2},${y2}`);
+ path.setAttribute('marker-end', 'url(#ah)');
+ arrowSvg.appendChild(path);
+ });
+}
+
+function clearHov() {
+ document.querySelectorAll('.cell.hov, .gcell.hov').forEach((d) =>
d.classList.remove('hov'));
+ arrowSvg.innerHTML = '';
+ hovFlat = null;
+}
+
+panels.addEventListener('click', (e) => {
+ const cell = e.target.closest('.cell') || e.target.closest('.gcell');
+ if (!cell || cell.dataset.flat === undefined) return;
+ const flat = +cell.dataset.flat;
+ if (hovFlat === flat) { clearHov(); draw(); return; }
+ clearHov();
+ hovFlat = flat;
+ draw();
+});
+
+// ── Legend ──────────────────────────────────────────────────────────────────
+function swatchEl(color) { const w = mk('div', 'swtch'); w.style.background =
color; return w; }
+
+function drawLegend() {
+ const lg = document.getElementById('lg');
+ lg.innerHTML = '';
+
+ // Color key: an actual swatch-per-value table using the same palette as the
grid.
+ const r0 = mk('div', 'leg-row');
+ const lead = mk('div', 'li');
+ lead.innerHTML = `<b>color = ${ST.colorAxis || 'physical'} value:</b>`;
+ r0.appendChild(lead);
+ const vals = ST.colorVals || [];
+ if (vals.length <= 8) {
+ for (const v of vals) {
+ const li = mk('div', 'li');
+ li.appendChild(swatchEl(paletteColor(v)));
+ li.appendChild(document.createTextNode(String(v)));
+ r0.appendChild(li);
+ }
+ } else {
+ for (let k = 0; k < 8; k++) {
+ const li = mk('div', 'li');
+ li.appendChild(swatchEl(PALETTE[k]));
+ li.appendChild(document.createTextNode('≡' + k));
+ r0.appendChild(li);
+ }
+ const note = mk('div', 'li');
+ note.textContent = `(${ST.colorAxis} mod 8; ${vals.length} values)`;
+ r0.appendChild(note);
+ }
+ lg.appendChild(r0);
+
+ const r1 = mk('div', 'leg-row');
+ const c2 = mk('div', 'li'); c2.textContent = 'number = logical element
index'; r1.appendChild(c2);
+ if (ST.layout.replica.length) {
+ r1.appendChild(mk('div', 'leg-sep'));
+ const c3 = mk('div', 'li');
+ c3.textContent = 'replica → identical copies: the same color appears in
multiple physical cells';
+ r1.appendChild(c3);
+ }
+ lg.appendChild(r1);
+
+ const r2 = mk('div', 'leg-row');
+ const m = mk('div', 'li');
+ const owners = axesUsed(ST.layout).filter(isOwnerAxis);
+ const mem = axesUsed(ST.layout).filter((a) => !isOwnerAxis(a));
+ m.textContent = 'owner axes: ' + (owners.join(', ') || '(none — pure memory
layout)') +
+ ' · memory axes: ' + (mem.join(', ') || '(none)');
+ r2.appendChild(m);
+ lg.appendChild(r2);
+}
+
+function escapeHtml(s) {
+ return s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>':
'>', '"': '"' }[c]));
+}
+
+// ── Presets + controls
───────────────────────────────────────────────────────
+// Case-study presets use scaled-down shapes so every element renders; the
+// mapping semantics match the full-size examples in the docs.
+const PRESETS = [
+ { label: 'Shard → lanes (intro)', shape: '4, 8', expr:
'S[(4,8):(8@laneid,1@laneid)]' },
+ { label: 'Shard + registers', shape: '4, 8', expr:
'S[(4,2,4):(8@laneid,1@m,1@laneid)]' },
+ { label: 'Shard + replica', shape: '4, 8', expr:
'S[(4,8):(8@laneid,1@laneid)] + R[2:1@warpid]' },
+ {
+ label: 'Tensor-core tile (doc example)', shape: '8, 16',
+ expr: 'S[(8,2,4,2):(4@laneid,1@warpid,1@laneid,1)] + R[2:4@warpid] +
5@warpid',
+ },
+ { label: 'Distributed 2×2 GPU mesh (pid)', shape: '4, 4', expr:
'S[(2,2,2,2):(1@pid,2@m,2@pid,1@m)]' },
+ { label: 'Mesh + replica (pid)', shape: '4, 4', expr:
'S[(2,2,4):(1@pid,2@m,1@m)] + R[2:2@pid]' },
+ { label: 'Accelerator scratchpad (P/F)', shape: '4, 8', expr:
'S[(2,4,4):(4@F,1@P,1@F)]' },
+ { label: 'Blackwell tensor memory (TLane/TCol)', shape: '4, 8', expr:
'S[(2,4,4):(4@TCol,1@TLane,1@TCol)]' },
+ { label: 'SMEM, no swizzle (bank conflicts)', shape: '8, 64', expr:
'S[(8,64):(64@m,1@m)]', dtype: 16, mode: 'none' },
+ { label: 'SMEM swizzle 128B (fp16)', shape: '8, 64', expr:
'S[(8,64):(64@m,1@m)]', dtype: 16, mode: '128' },
+ { label: '1-D shard', shape: '8', expr: 'S[8:4@laneid]' },
+ { label: 'Extents only (default strides)', shape: '8, 4', expr: 'S[(8,4)]' },
+];
+
+const DTYPES = [
+ { label: 'float16 (16-bit)', bits: 16 },
+ { label: 'bfloat16 (16-bit)', bits: 16 },
+ { label: 'float8 (8-bit)', bits: 8 },
+ { label: 'float32 (32-bit)', bits: 32 },
+ { label: 'tfloat32 (32-bit)', bits: 32 },
+ { label: 'float64 (64-bit)', bits: 64 },
+];
+const SWMODES = [
+ { label: 'off (general layout)', value: 'off' },
+ { label: 'none — raw banks', value: 'none' },
+ { label: '32B swizzle', value: '32' },
+ { label: '64B swizzle', value: '64' },
+ { label: '128B swizzle', value: '128' },
+];
+
+const shapeInput = document.getElementById('shape');
+const exprInput = document.getElementById('expr');
+const presetSel = document.getElementById('preset');
+const dtypeSel = document.getElementById('dtype');
+const swmodeSel = document.getElementById('swmode');
+
+function applyPreset(i) {
+ const p = PRESETS[i];
+ shapeInput.value = p.shape;
+ exprInput.value = p.expr;
+ if (dtypeSel) dtypeSel.value = String(p.dtype || 16);
+ if (swmodeSel) swmodeSel.value = p.mode || 'off';
+ refresh();
+}
+function refresh() { clearHov(); recompute(); draw(); }
+
+function init() {
+ PRESETS.forEach((p, i) => {
+ const o = document.createElement('option');
+ o.value = i; o.textContent = p.label; presetSel.appendChild(o);
+ });
+ DTYPES.forEach((d) => {
+ const o = document.createElement('option');
+ o.value = d.bits; o.textContent = d.label; dtypeSel.appendChild(o);
+ });
+ SWMODES.forEach((s) => {
+ const o = document.createElement('option');
+ o.value = s.value; o.textContent = s.label; swmodeSel.appendChild(o);
+ });
+ presetSel.addEventListener('change', () => applyPreset(+presetSel.value));
+ shapeInput.addEventListener('input', refresh);
+ exprInput.addEventListener('input', refresh);
+ dtypeSel.addEventListener('change', refresh);
+ swmodeSel.addEventListener('change', refresh);
+ window.addEventListener('resize', () => { resetFit(); fitEmbed();
postHeight(); });
+ window.addEventListener('load', () => { resetFit(); fitEmbed();
postHeight(); });
+ // Deep-linking for embeds: ?preset=<index|label-slug>¬itle
+ const params = new URLSearchParams(location.search);
+ let presetIdx = 0;
+ const want = params.get('preset');
+ if (want !== null) {
+ const slug = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g,
'-').replace(/^-|-$/g, '');
+ if (/^\d+$/.test(want)) {
+ presetIdx = Math.min(PRESETS.length - 1, Math.max(0, parseInt(want,
10)));
+ } else {
+ const w = slug(want);
+ const found = PRESETS.findIndex((p) => slug(p.label).includes(w));
+ if (found >= 0) presetIdx = found;
+ }
+ }
+ presetSel.value = presetIdx;
+ if (params.has('notitle')) document.body.classList.add('notitle');
+ if (params.has('lock')) document.body.classList.add('lock');
+ applyPreset(presetIdx);
+}
+
+init();
diff --git a/assets/tirx-layout-demo/viz-base.css
b/assets/tirx-layout-demo/viz-base.css
new file mode 100644
index 00000000000..81ffadc19cd
--- /dev/null
+++ b/assets/tirx-layout-demo/viz-base.css
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * Derived from mlsyscourse/slides-modern-gpu-programming
+ * (data-layout/site/viz-base.css). Shared base styles for the
+ * TIRx layout visualization; not derived from any third-party demo.
+ */
+
+/* Shared base styles for all viz HTMLs
+ * Colors: --bg gray, --accent blue, --surface white
+ * Fonts: Inter (body), SF Mono (code/notation)
+ */
+
+:root {
+ --bg:#fff; --surface:#fff; --border:#dfe1e6; --text:#222; --dim:#888;
--accent:#3b82f6;
+
+ /* ── Group palette (tiles, lanes, banks, sectors) ─────── */
+ --color-group-0: #5b9bd5;
+ --color-group-1: #ed9a3c;
+ --color-group-2: #d95555;
+ --color-group-3: #45b5a5;
+ --color-group-4: #5fb85f;
+ --color-group-5: #e0b828;
+ --color-group-6: #9d6eb5;
+ --color-group-7: #e87888;
+
+ /* ── Interaction ──────────────────────────────────────── */
+ --color-hover-bg: #dbeafe;
+ --color-hover-text: #1e40af;
+
+ /* ── Status ───────────────────────────────────────────── */
+ --color-good: #2e7d32;
+ --color-bad: #c62828;
+
+ /* ── Boundaries & neutrals ────────────────────────────── */
+ --color-boundary: #8C1515;
+ --color-cell-bg: #f0f1f3;
+ --color-cell-bg-alt: #e8eaed;
+}
+* { box-sizing:border-box; margin:0; padding:0; }
+body { background:var(--bg); color:var(--text); font-family:'Inter','SF
Pro','Segoe UI',system-ui,sans-serif; padding:24px; }
+body.figure h1, body.figure .sub { display:none; }
+body.notitle h1, body.notitle .sub { display:none; }
+h1 { text-align:center; font-size:21px; font-weight:700; margin-bottom:2px; }
+.sub { text-align:center; color:var(--dim); font-size:14px;
margin-bottom:20px; font-family:'SF Mono','Fira Code',monospace; }
+
+/* Controls */
+.controls { display:flex; gap:18px; justify-content:center;
align-items:center; margin-bottom:10px; flex-wrap:wrap; }
+.lbl { font-size:13px; color:var(--dim); margin-right:4px; font-weight:600; }
+.bg { display:inline-flex; gap:2px; }
+.btn {
+ padding:5px 11px; border:1px solid var(--border); background:var(--surface);
color:var(--text);
+ cursor:pointer; border-radius:5px; font-size:13px; font-family:inherit;
font-weight:500; transition:all .12s;
+}
+.btn:hover { background:#eef0f4; }
+.btn.on { background:var(--accent); border-color:var(--accent); color:#fff; }
+.btn.on:hover { background:#2563eb; }
+
+/* Side-by-side panels */
+.panels { display:grid; grid-template-columns:1fr 1fr; gap:20px;
max-width:1100px; margin:0 auto; position:relative; }
+.panel { background:var(--surface); border-radius:10px; padding:16px 14px;
border:1px solid var(--border);
+ box-shadow:0 1px 3px rgba(0,0,0,.06); }
+.panel h2 { text-align:center; font-size:14px; font-weight:700;
margin-bottom:2px; }
+.panel .nota { text-align:center; font-size:11px; color:var(--accent);
font-family:'SF Mono','Fira Code',monospace;
+ margin-bottom:8px; font-weight:600; }
+
+/* Grid cells */
+.grid { display:grid; gap:3px; }
+.hdr { font-size:10px; color:var(--dim); text-align:center; padding:2px 0;
font-weight:600; }
+.rl { font-size:10px; color:var(--dim); display:flex; align-items:center;
justify-content:flex-end; padding-right:3px; font-weight:600; }
+.cell {
+ aspect-ratio:1; border-radius:5px; display:flex; flex-direction:column;
align-items:center; justify-content:center;
+ font-size:13px; font-weight:700; border:2.5px solid transparent;
transition:all .18s;
+ min-width:0; cursor:pointer; line-height:1.15;
+}
+.cell.hov { border-color:#222; border-width:3px; z-index:2; box-shadow:0 0 8px
rgba(59,130,246,.4); }
+.cell.dm { opacity:.25; }
+
+/* Arrow SVG overlay */
+.arrow-svg { position:absolute; top:0; left:0; width:100%; height:100%;
pointer-events:none; z-index:10; overflow:visible; }
+.arrow-svg path { fill:none; stroke:#222; stroke-width:1.5; }
+.arrow-svg text { font-size:11px; font-weight:600; fill:#222;
font-family:inherit; }
+
+/* Formula bar */
+.formula-bar { max-width:1100px; margin:18px auto 0;
background:var(--surface); border-radius:10px;
+ padding:12px 18px; border:1px solid var(--border); box-shadow:0 1px 3px
rgba(0,0,0,.06); }
+.formula-bar .ftitle { font-size:13px; font-weight:700; margin-bottom:6px; }
+.formula-bar .fcontent { font-size:13px; font-family:'SF Mono','Fira
Code',monospace; color:var(--accent); line-height:1.6; }
+
+/* Legend */
+.leg { display:flex; flex-direction:column; align-items:center; gap:4px;
margin-top:14px; }
+.leg-row { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; }
+.leg-group { display:flex; gap:10px; align-items:center; }
+.leg-sep { width:1px; height:16px; background:var(--border); }
+.li { display:flex; align-items:center; gap:4px; font-size:12px;
color:var(--dim); }
+.swtch { width:14px; height:14px; border-radius:3px; }
+
+@media(max-width:700px) { .panels { grid-template-columns:1fr; } }
diff --git a/assets/tirx-layout-demo/viz-base.js
b/assets/tirx-layout-demo/viz-base.js
new file mode 100644
index 00000000000..c5ef10f13db
--- /dev/null
+++ b/assets/tirx-layout-demo/viz-base.js
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * Derived from mlsyscourse/slides-modern-gpu-programming
+ * (data-layout/site/viz-base.js). Shared behavior for the TIRx
+ * layout visualization; not derived from any third-party demo.
+ */
+
+// Shared behavior for all viz HTMLs
+document.addEventListener('DOMContentLoaded', function() {
+ var p = new URLSearchParams(location.search);
+ if (p.has('notitle')) document.body.classList.add('notitle');
+
+ // Forward arrow keys to parent (reveal.js) when embedded
+ if (window.parent !== window) {
+ document.addEventListener('keydown', function(e) {
+ if ([37, 38, 39, 40, 27, 32].indexOf(e.keyCode) !== -1) {
+ // Left, Up, Right, Down, Escape, Space
+ window.parent.postMessage({ type: 'revealKey', keyCode: e.keyCode },
'*');
+ }
+ });
+ }
+});
+
+// Auto-height: when embedded in the book, measure our own content height and
post
+// it to the parent so it can size the iframe to fit (no inner scrollbar). This
+// demo is responsive (it fills the iframe width), so only the HEIGHT needs to
+// follow content. Push-based, so it catches our own DOM changes (editing the
+// layout, clicking a cell, switching presets) that an outside observer can
miss.
+(function () {
+ if (window.parent === window) return;
+ var lastH = 0;
+ function report() {
+ var b = document.body, de = document.documentElement;
+ var h = (b ? b.scrollHeight : 0) || (de ? de.scrollHeight : 0) || 0;
+ if (h && Math.abs(h - lastH) > 1) {
+ lastH = h;
+ window.parent.postMessage({ type: 'demoHeight', height: h }, '*');
+ }
+ }
+ var scheduled = false;
+ function schedule() {
+ if (scheduled) return;
+ scheduled = true;
+ requestAnimationFrame(function () { scheduled = false; report(); });
+ }
+ try { new ResizeObserver(schedule).observe(document.documentElement); }
catch (e) {}
+ try {
+ new MutationObserver(schedule).observe(document.documentElement, {
+ subtree: true, childList: true, attributes: true, characterData: true
+ });
+ } catch (e) {}
+ document.addEventListener('DOMContentLoaded', schedule);
+ window.addEventListener('load', schedule);
+ window.addEventListener('click', function () { setTimeout(schedule, 0); },
true);
+ [100, 300, 600, 1200].forEach(function (t) { setTimeout(schedule, t); });
+})();