diff --git a/webgpu/lessons/resources/jsonml.js b/webgpu/lessons/resources/jsonml.js new file mode 100644 index 00000000..eff16f0e --- /dev/null +++ b/webgpu/lessons/resources/jsonml.js @@ -0,0 +1,80 @@ +/** + * Implements JsonML *like* element creation (http://www.jsonml.org/) + * + * The major difference is this takes event handlers for `on` functions + * and supports nested attributes? Also allows elements. + * + * ```js + * document.body.appendChild(makeElem([ + * 'style', + * '.bold { font-weight: bold; }', + * '.italic { font-style: italic; }', + * ])); + * + * document.body.appendChild(makeElem([ + * 'div', + * 'This next word is ', + * ['span', {style: 'color: red'}, 'red'], // style is string + * ' and this next word is ', + * ['span', {style: {color: 'blue'}}, 'blue'], // style is object + * ' and this next word is ', + * ['span', {className: 'bold'}, 'bold'], // className works + * ' and this next word is ', + * ['span', {class: 'italic bold'}, 'italic-bold'], // class works too? + * ])); + * + * document.body.appendChild(makeElem([ + * 'form', + * 'Enter name:', + * ['input', {type: 'text', placeholder: 'Jane Doe'}], + * [ + * 'button', + * { + * type: 'button', + * onClick: (e) => { + * console.log('name:', e.target.previousElementSibling.value); + * }, + * }, + * 'submit', + * ], + * ])); + * ``` + */ +export function makeElem(elemSpec) { + const tag = elemSpec[0]; + if (tag instanceof Node) { + return tag; + } + const elem = document.createElement(tag); + + let firstChildNdx = 1; + if (typeof elemSpec[1] !== Node && typeof elemSpec[1] !== 'string' && !Array.isArray(elemSpec[1])) { + firstChildNdx = 2; + for (const [key, value] of Object.entries(elemSpec[1])) { + if (typeof value === 'function' && key.startsWith('on')) { + const eventName = key.substring(2).toLowerCase(); + elem.addEventListener(eventName, value, {passive: false}); + } else if (typeof value === 'object') { + for (const [k, v] of Object.entries(value)) { + elem[key][k] = v; + } + } else if (elem[key] === undefined) { + elem.setAttribute(key, value); + } else { + elem[key] = value; + } + } + } + + for (let ndx = firstChildNdx; ndx < elemSpec.length; ++ndx) { + const v = elemSpec[ndx]; + if (typeof v === 'string') { + elem.appendChild(document.createTextNode(v)); + } else if (v instanceof Node) { + elem.appendChild(v); + } else { + elem.appendChild(makeElem(v)); + } + } + return elem; +} diff --git a/webgpu/lessons/resources/lesson.css b/webgpu/lessons/resources/lesson.css index 0df274e7..68f6a17b 100644 --- a/webgpu/lessons/resources/lesson.css +++ b/webgpu/lessons/resources/lesson.css @@ -998,6 +998,18 @@ pre.prettyprint.lighttheme .fun { color: #900; } /* function name */ } } +@media (width < 540px) { + .byte-diagram { + --byte-grid-size: 20px; + font-size: 12px; + } +} +@media (width < 400px) { + .byte-diagram { + --byte-grid-size: 16px; + } +} + /* disqus */ #disqus_thread { diff --git a/webgpu/lessons/webgpu-memory-layout.css b/webgpu/lessons/webgpu-memory-layout.css new file mode 100644 index 00000000..1fc7eac2 --- /dev/null +++ b/webgpu/lessons/webgpu-memory-layout.css @@ -0,0 +1,44 @@ +:root { + --whl-th-bg-color: lightblue; + --whl-row-bg-color: linear-gradient(#FFF, #EEE); +} +@media (prefers-color-scheme: dark) { + :root { + --whl-th-bg-color: blue; + --whl-row-bg-color: linear-gradient(#333, #111); + } +} +div[data-diagram="typedArrays"] { + font-family: monospace; + font-size: small; + display: flex; + justify-content: center; + flex-direction: column; + overflow-x: auto; + + label { display: flex; align-items: center;} + table { border-collapse: collapse; line-height: 1.5; width: max-content; } + thead { background-color: var(--whl-th-bg-color);} + th, td { border: 1px solid gray; position: relative; padding: 0.2em; margin: 0 } + input[type="text"] { border: none; font-family: monospace; background-color: inherit; width: 100%; padding: 0; margin: 0; text-align: right; } + .error { background-color: red; } + + /* + tr:nth-child(even) { background-color: #333;} + tr:nth-child(odd) { background-color: #222;} + */ + tr { background: var(--whl-row-bg-color); } + + td[colspan="1"] { width: 3em } + td[colspan="2"] { width: 6em } + td[colspan="4"] { width: 12em } + td[colspan="8"] { width: 24em } +} + +@media (width < 740px) { + div[data-diagram="typedArrays"] { + font-size: x-small; + display: block; + } +} + diff --git a/webgpu/lessons/webgpu-memory-layout.js b/webgpu/lessons/webgpu-memory-layout.js index 8bf13858..1da90796 100644 --- a/webgpu/lessons/webgpu-memory-layout.js +++ b/webgpu/lessons/webgpu-memory-layout.js @@ -11,6 +11,9 @@ import { import { makeTable, } from './resources/elem.js'; +import { + makeElem +} from './resources/jsonml.js'; import typeInfo from './resources/wgsl-data-types.js'; renderDiagrams({ @@ -93,4 +96,161 @@ renderDiagrams({ addRow([name, size, align]); } }, + + typedArrays(elem) { + const viewCtors = [ + Int8Array, + Uint8Array, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + BigInt64Array, + BigUint64Array, + ]; + + const numBytes = 16; + + const range = (num, fn) => new Array(num).fill(0).map((_, i) => fn(i)); + + const arrayBuffer = new ArrayBuffer(numBytes); + const views = viewCtors.map(Ctor => new Ctor(arrayBuffer)); + const f32 = new Float32Array(arrayBuffer); + f32.set([123, -456, 7.89, -0.123]); + + const updateFns = []; + function updateAll() { + for (const fn of updateFns) { + fn(); + } + } + + let showAsHex = false; + + function format(view, v) { + if (showAsHex && view.constructor.name.includes('nt')) { + return v.toString(16);//.padStart(view.BYTES_PER_ELEMENT * 2, '0'); + } else { + return v.toString(); + } + } + + function parseBigInt16(v) { + v = v.trim(); + const [start, sign] = v[0] === '-' + ? [1, -1] + : [0, 1]; + let result = BigInt(0); + for (let i = start; i < v.length; ++i) { + const c = v[i].toLowerCase().charCodeAt(0); + let digit; + if (c >= 0x30 && c <= 0x39) { + digit = c - 0x30; + } else if (c >= 0x61 && c <= 0x66) { + digit = c - 0x61 + 10; + } else { + throw new Error('not hex'); + } + result = result * BigInt(0x10) + BigInt(digit); + } + return result * BigInt(sign); + } + + function parseBigInt(v) { + return showAsHex ? parseBigInt16(v) : BigInt(v); + } + + const intRE = /^-?\d+$/; + const hexRE = /^-?[0-9a-f]+$/i; + function parseIntNumber(v) { + if (showAsHex) { + if (!hexRE.test(v)) { + throw new Error('not hex'); + } + } else { + if (!intRE.test(v)) { + throw new Error('not int'); + } + } + return parseInt(v, showAsHex ? 16 : 10); + } + + function parseFloatNumber(v) { + v = Number(v); + if (isNaN(v)) { + throw Error('NaN'); + } + return v; + } + + elem.appendChild(makeElem([ + 'table', + [ + 'thead', + ['th', 'arrayBuffer'], + ...range(numBytes, i => ['th', `${i}`]), + ], + [ + 'tbody', + ...views.map((view) => { + const numCellsPerElem = view.BYTES_PER_ELEMENT; + const numElems = numBytes / numCellsPerElem; + const parse = view instanceof BigInt64Array + ? parseBigInt + : view instanceof BigUint64Array + ? parseBigInt + : view.constructor.name.includes('Float') + ? parseFloatNumber + : parseIntNumber; + return [ + 'tr', + ['td', `as${view.constructor.name.substring(0, view.constructor.name.length - 5)}` ], + ...range(numElems, i => { + const input = makeElem([ + 'input', {type: 'text', value: 123, onInput: function() { + let err = false; + try { + view[i] = parse(this.value); + } catch (error) { + //console.log('here', error.message); + err = true; + } + input.classList.toggle('error', err); + updateAll(); + }, + }, + ]); + + updateFns.push(() => { + if (document.activeElement !== input) { + input.value = format(view, view[i]); + input.classList.remove('error'); + } + }); + + return ['td', {colSpan: numCellsPerElem}, + input, + ]; + }), + ]; + }), + ], + ])); + + updateAll(); + + elem.appendChild(makeElem(['label', + elem.dataset.caption, + [ + 'input', { type: 'checkbox', onChange: function() { + showAsHex = this.checked; + updateAll(); + }, + }, + ], + ])); + + }, }); \ No newline at end of file diff --git a/webgpu/lessons/webgpu-memory-layout.md b/webgpu/lessons/webgpu-memory-layout.md index 046e2d22..4896e04f 100644 --- a/webgpu/lessons/webgpu-memory-layout.md +++ b/webgpu/lessons/webgpu-memory-layout.md @@ -78,8 +78,8 @@ ourStructValuesAsU32[kFrameCountOffset] = 56; // an integer value ## `TypedArrays` -Note, like many things in programming there are multiple ways we could -do this. `TypedArray`s have a constructor that takes various forms. For example +Like many things in programming there are multiple ways we could +set the data for `OutStruct`. `TypedArray`s have a constructor that takes various forms. For example * `new Float32Array(12)` @@ -109,7 +109,7 @@ do this. `TypedArray`s have a constructor that takes various forms. For example console.log(u32s); // produces 0, 0, 1, 1, 1 ``` - The reason is you can't put values like 0.8 and 1.2 into a `Uint32Array` + The reason is you can't put values like 0.8 and 1.2 into a `Uint32Array`. They get converted to unsigned integers * `new Float32Array(someArrayBuffer)` @@ -201,6 +201,32 @@ console.log(Array.from(u32).map(v => v.toString(16).padStart(8, '0'))); The values above are the 32bit hex representations of the floating point values for 1, 1000, -1000 +For example: Let's create a 16 byte `ArrayBuffer`. Then we'll create different +`TypedArray` views of the same memory. + +```js +const arrayBuffer = new ArrayBuffer(16); +const asInt8 = new Int8Array(arrayBuffer); +const asUint8 = new Uint8Array(arrayBuffer); +const asInt16 = new Int16Array(arrayBuffer); +const asUint16 = new Uint16Array(arrayBuffer); +const asInt32 = new Int32Array(arrayBuffer); +const asUint32 = new Uint32Array(arrayBuffer); +const asFloat32 = new Float32Array(arrayBuffer); +const asFloat64 = new Float64Array(arrayBuffer); +const asBigInt64 = new BigInt64Array(arrayBuffer); +const asBigUint64 = new BigInt64Array(arrayBuffer); + +// Set some values to start. +asFloat32.set([123, -456, 7.8, -0.123]); +``` + +Here's a representation of all of those views, all viewing the same +memory. Below, edit any one number and the corresponding values that are +using the same memory will change. + +
+ ## `map` issues Be aware, the `map` function of a `TypedArray` makes a new typed array of the same type! @@ -464,4 +490,5 @@ If you do want to do it manually though, [here's a page that will compute the offsets for you](resources/wgsl-offset-computer.html) +