diff --git a/docs/_includes/mesh-band.html b/docs/_includes/mesh-band.html
index 37cfef4..a979d4a 100644
--- a/docs/_includes/mesh-band.html
+++ b/docs/_includes/mesh-band.html
@@ -15,7 +15,7 @@
data-distortion="{{ include.distortion | default: 0.8 }}"
data-swirl="{{ include.swirl | default: 0.55 }}">
-
+
{{ include.caption }}
Field: Paper Shaders mesh gradient (Apache-2.0), palette and weights from this note.
diff --git a/docs/assets/figures/mage-004/mesh-band.png b/docs/assets/figures/mage-004/mesh-band.png
index 64a234a..325ef63 100644
Binary files a/docs/assets/figures/mage-004/mesh-band.png and b/docs/assets/figures/mage-004/mesh-band.png differ
diff --git a/docs/assets/figures/mage-007/mesh-band.png b/docs/assets/figures/mage-007/mesh-band.png
index 7298492..210cb28 100644
Binary files a/docs/assets/figures/mage-007/mesh-band.png and b/docs/assets/figures/mage-007/mesh-band.png differ
diff --git a/docs/assets/mesh-band.js b/docs/assets/mesh-band.js
index 98879a9..1eb4e75 100644
--- a/docs/assets/mesh-band.js
+++ b/docs/assets/mesh-band.js
@@ -30,6 +30,8 @@ uniform float u_grainMixer;
uniform float u_grainOverlay;
uniform float u_intensity;
uniform float u_opacityGain;
+uniform float u_saturation;
+uniform vec2 u_pointer;
in vec2 v_objectUV;
out vec4 fragColor;
@@ -105,7 +107,8 @@ void main() {
for (int i = 0; i < 10; i++) {
if (i >= int(u_colorsCount)) break;
- vec2 pos = getPosition(i, t) + mixerGrain;
+ // The field leans toward the pointer: the nearer spot leads, the far ones trail.
+ vec2 pos = getPosition(i, t) + mixerGrain + u_pointer * (0.18 / (1.0 + float(i)));
vec3 colorFraction = u_colors[i].rgb * u_colors[i].a;
float opacityFraction = u_colors[i].a;
@@ -127,6 +130,11 @@ void main() {
color *= u_intensity;
opacity *= u_opacityGain;
+ // Paper's shader averages the spots, which on a page of two or three colours
+ // drags everything toward grey. Push the result back away from its own luma so
+ // the band reads as colour rather than as a wash.
+ color = mix(vec3(dot(color, vec3(0.2126, 0.7152, 0.0722))), color, u_saturation);
+
if (u_grainOverlay > 0.) {
float grainOverlay = valueNoise(rotate(grainUV, 1.) + vec2(3.));
grainOverlay = mix(grainOverlay, valueNoise(rotate(grainUV, 2.) + vec2(-1.)), .5);
@@ -254,6 +262,8 @@ void main() {
grainOverlay: gl.getUniformLocation(prog, "u_grainOverlay"),
intensity: gl.getUniformLocation(prog, "u_intensity"),
opacityGain: gl.getUniformLocation(prog, "u_opacityGain"),
+ saturation: gl.getUniformLocation(prog, "u_saturation"),
+ pointer: gl.getUniformLocation(prog, "u_pointer"),
};
gl.uniform4fv(u.colors, spots);
@@ -262,12 +272,18 @@ void main() {
gl.uniform1f(u.swirl, Number(band.dataset.swirl ?? 0.55));
gl.uniform1f(u.grainMixer, Number(band.dataset.grainMixer ?? 0.05));
gl.uniform1f(u.grainOverlay, Number(band.dataset.grainOverlay ?? 0.04));
- gl.uniform1f(u.intensity, Number(band.dataset.intensity ?? 0.8));
- gl.uniform1f(u.opacityGain, Number(band.dataset.opacity ?? 0.6));
+ gl.uniform1f(u.intensity, Number(band.dataset.intensity ?? 0.5));
+ gl.uniform1f(u.opacityGain, Number(band.dataset.opacity ?? 0.85));
+ gl.uniform1f(u.saturation, Number(band.dataset.saturation ?? 2.9));
+ gl.uniform2f(u.pointer, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(...BACKDROP);
+ // Pointer state: a direction the spots lean toward, eased into and out of.
+ const pointer = [0, 0];
+ const pointerTarget = [0, 0];
+
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
@@ -283,6 +299,10 @@ void main() {
function frame(seconds) {
resize();
+ // The pointer pulls the field, then the field settles back on its own.
+ pointer[0] += (pointerTarget[0] - pointer[0]) * 0.07;
+ pointer[1] += (pointerTarget[1] - pointer[1]) * 0.07;
+ gl.uniform2f(u.pointer, pointer[0], pointer[1]);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform1f(u.time, seconds);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
@@ -333,6 +353,22 @@ void main() {
window.addEventListener("resize", () => {
if (reduced.matches) frame(0);
});
+
+ // A mouse leans the field; lifts of the pointer let it settle back. Touch is
+ // left alone so the band never competes with scrolling.
+ if (!reduced.matches) {
+ band.addEventListener("pointermove", (event) => {
+ if (event.pointerType && event.pointerType !== "mouse") return;
+ const rect = canvas.getBoundingClientRect();
+ if (!rect.width || !rect.height) return;
+ pointerTarget[0] = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
+ pointerTarget[1] = -((event.clientY - rect.top) / rect.height - 0.5) * 2;
+ });
+ band.addEventListener("pointerleave", () => {
+ pointerTarget[0] = 0;
+ pointerTarget[1] = 0;
+ });
+ }
}
function boot() {
diff --git a/docs/assets/notebook.css b/docs/assets/notebook.css
index 2c2f01a..110c80e 100644
--- a/docs/assets/notebook.css
+++ b/docs/assets/notebook.css
@@ -168,7 +168,7 @@ mjx-container[display] { overflow-x: auto; overflow-y: hidden; padding-block: 8p
radial-gradient(90% 130% at 52% 92%, rgba(147, 202, 255, .16), transparent 58%),
var(--paper);
}
-.mesh-band canvas { display: block; width: 100%; height: auto; aspect-ratio: 740 / 220; opacity: 0; transition: opacity .7s ease; }
+.mesh-band canvas { display: block; width: 100%; height: auto; aspect-ratio: 740 / 220; max-height: 170px; opacity: 0; transition: opacity .7s ease; }
.mesh-band[data-ready] canvas { opacity: 1; }
.mesh-band noscript img { display: block; width: 100%; height: auto; }
.mesh-band figcaption { padding: 10px 14px 12px; border-top: 1px solid var(--rule); background: var(--surface); font-size: 12px; line-height: 1.7; color: var(--muted); }
diff --git a/docs/experiments/mage-004.md b/docs/experiments/mage-004.md
index baa56db..38d0b6c 100644
--- a/docs/experiments/mage-004.md
+++ b/docs/experiments/mage-004.md
@@ -11,7 +11,7 @@ mesh_band: true
weights="0.74,1,0.62,0.90"
still="/assets/figures/mage-004/mesh-band.png"
alt="A dark field with four soft spots of colour — blue, green, lavender and warm sand — scaled by how competitive each implementation is."
- caption="The four spots are the four implementations in the table below, opacity set by the geometric mean of their kernel times relative to the best implementation on each operation: Triton brightest, cuTile Rust dimmest." %}**The claim.** Give a compiler the job of deciding how a GPU kernel places its data, and it will do
+ caption="The four spots are the four implementations, opacity set by how competitive each is." %}**The claim.** Give a compiler the job of deciding how a GPU kernel places its data, and it will do
a good job where the reuse is low and a worse one where the reuse is high. On five FP32 operations,
the [cuTile Rust](https://github.com/NVlabs/cutile-rs) tile kernels beat the hand-written ones on
bias + GELU ($8.19\ \mu s$ against $11.0$), draw on layer normalization ($10.76$ against $10.05$),
diff --git a/docs/experiments/mage-007.md b/docs/experiments/mage-007.md
index 3530c61..b29e347 100644
--- a/docs/experiments/mage-007.md
+++ b/docs/experiments/mage-007.md
@@ -11,7 +11,7 @@ mesh_band: true
weights="1.34,1.37,0.38"
still="/assets/figures/mage-007/mesh-band.png"
alt="A dark field with three soft spots of colour — green, blue and muted red — the red one much dimmer than the other two."
- caption="The three spots are this note's three changes, opacity set by the speed-up each measured: neighbour aggregation (1.34×, green), LayerNorm (1.37×, blue) and the rejected GELU variant (0.38×, red)." %}**The claim.** Making each thread load four values at once instead of one is a memory decision, not
+ caption="The three spots are this note's three changes, opacity set by the speed-up each measured." %}**The claim.** Making each thread load four values at once instead of one is a memory decision, not
an optimisation: it cuts the number of load instructions by four and puts $4\times$ the bytes in
flight per instruction. It pays when a kernel is short of instructions to issue. It costs when the
grid already fills the machine and the only thing hiding memory latency is how many threads are