diff --git a/rollup.config.js b/rollup.config.js index 11eba1e..15c6fa2 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -75,7 +75,10 @@ export default [ include: /node_modules/, requireReturnsDefault: 'auto', // <---- this solves default issue }), - typescript({ tsconfig: './tsconfig.json', sourceRoot: '/src' }), + typescript({ + tsconfig: './tsconfig.json', + sourceRoot: '/src', + }), postcss({ minimize: true, sourceMap: true, diff --git a/src/replay/lib.ts b/src/replay/lib.ts index 297bca6..fde19bd 100644 --- a/src/replay/lib.ts +++ b/src/replay/lib.ts @@ -176,6 +176,7 @@ export class Replay { case 'present': // Nothing to do? + this.state = { currentTexture: command.args.texture }; // TOOD: hack-remove break; case 'textureDestroy': @@ -217,7 +218,8 @@ export class Replay { return [this.commands.length]; } - replayTo(path) { + async replayTo(path) { + this.state = {}; // TODO: hack, remove // TODO resetState path = path.slice(); const replayLevel = path.shift(); @@ -234,6 +236,7 @@ export class Replay { this.execute(c); } } + return this.state; // TODO: hack, remove } getState() { @@ -272,6 +275,7 @@ class ReplayRenderPass extends ReplayObject { const c = window.structuredClone(command); switch (c.name) { case 'endPass': + this.state = {}; // TODO: hack, remove this.commands.push(c); return; @@ -486,6 +490,7 @@ class ReplayCommandBuffer extends ReplayObject { const c = window.structuredClone(command); switch (c.name) { case 'beginRenderPass': { + this.state = { currentRenderPassDescriptor: c.args }; // TODO: hack, remove for (const a of c.args.colorAttachments) { a.viewState = this.replay.textureViews[a.viewSerial]; a.view = a.viewState.webgpuObject; diff --git a/src/ui/contexts/UIStateContext.tsx b/src/ui/contexts/UIStateContext.tsx index de62da2..c1b1781 100644 --- a/src/ui/contexts/UIStateContext.tsx +++ b/src/ui/contexts/UIStateContext.tsx @@ -1,8 +1,11 @@ import React from 'react'; -import StepsVis from '../views/StepsVis/StepsVis'; -import ResultVis from '../views/ResultVis/ResultVis'; import ReplayAPI from '../ReplayAPI'; import { Replay } from '../../replay'; +import { getPathForLastStep } from '../lib/replay-utils'; + +import StateVis from '../views/StateVis/StateVis'; +import StepsVis from '../views/StepsVis/StepsVis'; +import ResultVis from '../views/ResultVis/ResultVis'; export type PaneComponent = React.FunctionComponent<{ data: any }> | React.ComponentClass<{ data: any }>; type ViewData = { @@ -11,11 +14,15 @@ type ViewData = { }; export type PaneIdToViewType = Record; +export type ReplayInfo = { + replay: Replay; + lastPath: number[]; +}; export type UIState = { paneIdToViewType: PaneIdToViewType; fullUI: boolean; - replays: Replay[]; + replays: ReplayInfo[]; }; export type SetStateArgs = Partial; @@ -117,18 +124,20 @@ export class UIStateHelper { }; addReplay = (replay: Replay) => { + const lastPath = getPathForLastStep(replay); + const replayInfo = { replay, lastPath }; this.setState({ - replays: [...this.state.replays, replay], + replays: [...this.state.replays, replayInfo], }); - this.setReplay(replay); + this.setReplay(replayInfo); }; - setReplay = (replay: Replay) => { + setReplay = (replayInfo: ReplayInfo) => { const paneId = this.getMostRecentPaneIdForComponentType(StepsVis); if (!paneId) { throw new Error('TODO: add pane of this type'); } - this.setPaneViewType(paneId, StepsVis, replay); + this.setPaneViewType(paneId, StepsVis, replayInfo); this.setFullUI(true); }; @@ -140,8 +149,17 @@ export class UIStateHelper { this.setPaneViewType(paneId, ResultVis, canvas); }; - playTo(replay: Replay, id: number[]) { - this.replayAPI?.playTo(replay, id); + setGPUState = (state: any) => { + const paneId = this.getMostRecentPaneIdForComponentType(StateVis); + if (!paneId) { + throw new Error('TODO: add pane of this type'); + } + this.setPaneViewType(paneId, StateVis, state); + }; + + async playTo(replay: Replay, path: number[]) { + const gpuState = await replay.replayTo(path); + this.setGPUState(gpuState); } } diff --git a/src/ui/delme-fudge.ts b/src/ui/delme-fudge.ts deleted file mode 100644 index 7ac0ca8..0000000 --- a/src/ui/delme-fudge.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* -This file should probably be deleted. -It exists because lib.js is not typescript -so it's casually describing the parts of -the replay code that the UI is accessing. -*/ -/* -export type CommandArgs = Record; - -export type Command = { - name: string; - args: CommandArgs; -}; - -export type Replay = { - commands: Command[]; -}; - -export type CommandBuffer = { - commands: Command[]; -}; - -export type QueueSubmitArgs = { - commandBuffers: CommandBuffer[]; -}; - -export type RenderPassArgs = { - commands: Command[]; -}; -*/ diff --git a/src/ui/index.ts b/src/ui/index.ts index ce89878..d4f2086 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -1 +1,2 @@ +import './lib/unwrapped-webgpu'; import './main'; diff --git a/src/ui/lib/replay-utils.ts b/src/ui/lib/replay-utils.ts new file mode 100644 index 0000000..90ebc20 --- /dev/null +++ b/src/ui/lib/replay-utils.ts @@ -0,0 +1,20 @@ +import { Replay } from '../../replay'; + +function getLastElementAndPushIndex(arr: any[], path: number[]) { + const lastNdx = arr.length - 1; + path.push(lastNdx); + return arr[lastNdx]; +} + +export function getPathForLastStep(replay: Replay) { + const path: number[] = []; + const lastCmd = getLastElementAndPushIndex(replay.commands, path); + if (lastCmd.name === 'queueSubmit') { + const lastCB = getLastElementAndPushIndex(lastCmd.args.commandBuffers, path); + const lastCBCmd = getLastElementAndPushIndex(lastCB.commands, path); + if (lastCBCmd.name === 'renderPass') { + getLastElementAndPushIndex(lastCBCmd.renderPass.commands, path); + } + } + return path; +} diff --git a/src/ui/lib/unwrapped-webgpu.ts b/src/ui/lib/unwrapped-webgpu.ts new file mode 100644 index 0000000..31130fc --- /dev/null +++ b/src/ui/lib/unwrapped-webgpu.ts @@ -0,0 +1,140 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +if (!GPUDevice.prototype.createBuffer.toString().includes('native')) { + throw new Error('This must run before the context is wrapped!'); +} + +type AnyFunc = (...args: any[]) => any; +type FuncsByName = Record; + +const origFnsByClass = new Map(); + +const webGPUClasses: Function[] = [ + GPUAdapter, + GPUBuffer, + GPUCommandEncoder, + GPUCanvasContext, + GPUDevice, + GPUQuerySet, + GPUQueue, + GPURenderPassEncoder, + GPURenderPipeline, + GPUTexture, +]; + +for (const Class of webGPUClasses) { + const origFns: FuncsByName = {}; + const proto = Class.prototype; + for (const name in proto) { + const props = Object.getOwnPropertyDescriptor(proto, name); + if (!props?.writable || typeof proto[name] !== 'function') { + continue; + } + origFns[name] = proto[name]; + } +} + +type ClassFnNamesA = [Function, string[]]; +type ClassFnNames = [Function, Set]; + +const cfn: ClassFnNamesA[] = [ + [GPU, ['requestAdapter']], + [GPUAdapter, ['requestDevice']], + [ + GPUDevice, + [ + 'createBuffer', + 'createTexture', + 'createSampler', + 'importExternalTexture', + 'createBindGroupLayout', + 'createPipelineLayout', + 'createBindGroup', + 'createShaderModule', + 'createComputePipeline', + 'createRenderPipeline', + 'createComputePipelineAsync', + 'createRenderPipelineAsync', + 'createCommandEncoder', + 'createRenderBundleEncoder', + 'createQuerySet', + ], + ], + [GPUCommandEncoder, ['beginRenderPass', 'beginComputePass', 'finish']], + [GPUCanvasContext, ['getCurrentTexture']], + [GPUTexture, ['createView']], +]; + +const classToCreationFunctionNames: ClassFnNames[] = (() => + cfn.map(([Class, names]) => [Class, new Set(names)]))(); + +const mapClassToCreationFunctionNames = new Map>(classToCreationFunctionNames); + +const isPromise = (p: any) => typeof p === 'object' && typeof p.then === 'function'; + +/** + * The prototype to this object may have been altered so we + * put properties on the object itself with the original functions. + * + * @param result The result of a function call + * @returns result with original methods added as properties + */ +function addOriginalFunctionsToResult(result: any): any { + if (typeof result !== 'object') { + return result; + } + + const Class = result.prototype.constructor; + const origFns = origFnsByClass.get(Class); + if (!origFns) { + return result; + } + + const createFns = mapClassToCreationFunctionNames.get(Class); + + for (const [fnName, origFn] of Object.entries(origFns)) { + if (createFns && createFns.has(fnName)) { + result[fnName] = function (...args: any[]) { + const result = origFn.call(this, ...args); + if (isPromise(result)) { + return result.then(addOriginalFunctionsToResult); + } + return addOriginalFunctionsToResult(result); + }; + } else { + result[fnName] = origFn; + } + } + + return result; +} + +const unwrappedDevices = new WeakMap(); + +export function getNonWrappedGPUDeviceForWrapped(wrapped: GPUDevice): GPUDevice { + const unwrappedDevice = unwrappedDevices.get(wrapped); + if (unwrappedDevice) { + return unwrappedDevice; + } + + const wrappedT = wrapped as Record; + const origFns = origFnsByClass.get(GPUDevice)!; + const obj: Record = {}; + for (const name in wrappedT) { + const props = Object.getOwnPropertyDescriptor(wrappedT, name); + if (!props?.writable || typeof wrappedT[name] !== 'function') { + obj[name] = wrappedT[name]; + } else { + const origFn = origFns[name]; + obj[name] = function (...args: any[]) { + const result = origFn.call(this, ...args); + if (isPromise(result)) { + return result.then(addOriginalFunctionsToResult); + } + return addOriginalFunctionsToResult(result); + }; + } + } + + return obj as GPUDevice; +} diff --git a/src/ui/main.tsx b/src/ui/main.tsx index 0966bb4..045d83e 100644 --- a/src/ui/main.tsx +++ b/src/ui/main.tsx @@ -40,33 +40,8 @@ uiStateHelper.registerAPI({ const replay = await loadReplay(trace); console.log(replay); - function getLastElementAndPushIndex(arr: any[], path: number[]) { - const lastNdx = arr.length - 1; - path.push(lastNdx); - return arr[lastNdx]; - } - - function getPathForLastStep(replay: Replay) { - const path: number[] = []; - const lastCmd = getLastElementAndPushIndex(replay.commands, path); - if (lastCmd.name === 'queueSubmit') { - const lastCB = getLastElementAndPushIndex(lastCmd.args.commandBuffers, path); - const lastCBCmd = getLastElementAndPushIndex(lastCB.commands, path); - if (lastCBCmd.name === 'renderPass') { - getLastElementAndPushIndex(lastCBCmd.renderPass.commands, path); - } - } - return path; - } - uiStateHelper.addReplay(replay); - const pathForLastStep = getPathForLastStep(replay); - console.log('path:', pathForLastStep); - - //const state = await replay.replayTo(pathForLastStep); - //console.log(state); - // Go through each command, and show the presented texture of the trace on the capture canvas. const captureCanvas = document.createElement('canvas'); const context = captureCanvas.getContext('webgpu')!; diff --git a/src/ui/views/ResultVis/ResultVis.tsx b/src/ui/views/ResultVis/ResultVis.tsx index 9a71b91..ec9a4ad 100644 --- a/src/ui/views/ResultVis/ResultVis.tsx +++ b/src/ui/views/ResultVis/ResultVis.tsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import './ResultVis.css'; -type Props = { +type ResultsVisProps = { data: HTMLCanvasElement; }; @@ -24,7 +24,7 @@ const draw = (ctx: CanvasRenderingContext2D, srcCanvas: HTMLCanvasElement) => { } }; -const ResultVis = ({ data }: Props) => { +const ResultVis = ({ data }: ResultsVisProps) => { const canvasRef = useRef(null); useEffect(() => { diff --git a/src/ui/views/StateVis/StateVis.css b/src/ui/views/StateVis/StateVis.css new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/StateVis/StateVis.tsx b/src/ui/views/StateVis/StateVis.tsx new file mode 100644 index 0000000..595a6b9 --- /dev/null +++ b/src/ui/views/StateVis/StateVis.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import './StateVis.css'; + +export default function StateVis() { + return
state vis
; +} diff --git a/src/ui/views/StepsVis/StepsVis.tsx b/src/ui/views/StepsVis/StepsVis.tsx index 3856fa8..1e769cf 100644 --- a/src/ui/views/StepsVis/StepsVis.tsx +++ b/src/ui/views/StepsVis/StepsVis.tsx @@ -1,6 +1,6 @@ -import React, { useState, useContext } from 'react'; +import React, { useState, useContext, useEffect } from 'react'; import { Command, CommandArgs, QueueSubmitArgs, RenderPassArgs, Replay } from '../../../replay'; -import { UIStateContext } from '../../contexts/UIStateContext'; +import { ReplayInfo, UIStateContext } from '../../contexts/UIStateContext'; import { classNames } from '../../lib/css'; import './StepsVis.css'; @@ -162,20 +162,30 @@ function Commands({ commands, commandId }: { commands: Command[]; commandId: num ); } -export default function StepsVis({ data }: { data: Replay }) { +interface StepsVisProps { + data: ReplayInfo; +} + +export default function StepsVis({ data }: StepsVisProps) { + const { replay, lastPath } = data; const { helper } = useContext(UIStateContext); const [state, setState] = useState({ currentStep: [], }); + const playTo = (step: number[]) => { setState({ currentStep: step }); - helper.playTo(data, step); + helper.playTo(replay, step); }; + useEffect(() => { + playTo(lastPath); + }, [data]); + return (
- {data ? : 'no replay'} + {data ? : 'no replay'}
);