diff --git a/renderer/image-textures.ts b/renderer/image-textures.ts index 21ca4ee..c508077 100644 --- a/renderer/image-textures.ts +++ b/renderer/image-textures.ts @@ -7,11 +7,17 @@ export type ImageTextureRenderer = { dispose(): void; }; -type TextureEntry = +type TextureState = | { status: "loading"; image: HTMLImageElement } | { status: "ready"; texture: WebGLTexture } | { status: "error" }; +type TextureEntry = { + source: string; + current: TextureState; + previousTexture?: WebGLTexture; +}; + export function createImageTextureRenderer(context: WebGlRendererContext, invalidate: () => void): ImageTextureRenderer { const { gl } = context; const program = createProgram(gl); @@ -30,11 +36,11 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali return { syncAssets(assets) { - const activeKeys = new Set(assets.map(textureKey)); - for (const [key, entry] of textures) { - if (!activeKeys.has(key)) { + const activeIds = new Set(assets.map((asset) => asset.id)); + for (const [assetId, entry] of textures) { + if (!activeIds.has(assetId)) { disposeEntry(gl, entry); - textures.delete(key); + textures.delete(assetId); } } }, @@ -43,7 +49,8 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true; const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed); - if (entry.status !== "ready") return false; + const texture = renderableTexture(entry); + if (!texture) return false; gl.enable(gl.SCISSOR_TEST); gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h); @@ -52,7 +59,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali gl.useProgram(program); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, entry.texture); + gl.bindTexture(gl.TEXTURE_2D, texture); gl.uniform1i(samplerLocation, 0); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); @@ -85,42 +92,71 @@ function getTextureEntry( invalidate: () => void, isDisposed: () => boolean, ): TextureEntry { - const key = textureKey(asset); - const cached = textures.get(key); - if (cached) return cached; + const cached = textures.get(asset.id); + if (cached && cached.source === asset.source) return cached; + if (cached) { + if (cached.current.status === "ready") { + if (cached.previousTexture) context.gl.deleteTexture(cached.previousTexture); + cached.previousTexture = cached.current.texture; + } else { + disposeState(context.gl, cached.current); + } + cached.source = asset.source; + cached.current = loadTextureState(context, asset, cached, invalidate, isDisposed); + return cached; + } + + const entry: TextureEntry = { source: asset.source, current: { status: "error" } }; + entry.current = loadTextureState(context, asset, entry, invalidate, isDisposed); + textures.set(asset.id, entry); + return entry; +} + +function loadTextureState( + context: WebGlRendererContext, + asset: Asset, + entry: TextureEntry, + invalidate: () => void, + isDisposed: () => boolean, +): TextureState { const image = new Image(); - textures.set(key, { status: "loading", image }); + const source = asset.source; image.onload = () => { - if (isDisposed()) return; + if (isDisposed() || entry.source !== source) return; const texture = createTexture(context.gl, image); - disposeEntry(context.gl, textures.get(key)); - textures.set(key, { status: "ready", texture }); + if (entry.previousTexture) context.gl.deleteTexture(entry.previousTexture); + entry.previousTexture = undefined; + entry.current = { status: "ready", texture }; invalidate(); }; image.onerror = () => { - if (isDisposed()) return; - textures.set(key, { status: "error" }); + if (isDisposed() || entry.source !== source) return; + entry.current = { status: "error" }; invalidate(); }; - image.src = asset.source; - + image.src = source; return { status: "loading", image }; } -function textureKey(asset: Asset) { - return `${asset.id}:${asset.source}`; +function renderableTexture(entry: TextureEntry): WebGLTexture | undefined { + if (entry.current.status === "ready") return entry.current.texture; + return entry.previousTexture; } -function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry | undefined) { - if (!entry) return; - if (entry.status === "ready") { - gl.deleteTexture(entry.texture); +function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry) { + disposeState(gl, entry.current); + if (entry.previousTexture) gl.deleteTexture(entry.previousTexture); +} + +function disposeState(gl: WebGL2RenderingContext, state: TextureState) { + if (state.status === "ready") { + gl.deleteTexture(state.texture); return; } - if (entry.status === "loading") { - entry.image.onload = null; - entry.image.onerror = null; + if (state.status === "loading") { + state.image.onload = null; + state.image.onerror = null; } } diff --git a/view/canvas/useCanvasInput.ts b/view/canvas/useCanvasInput.ts index 7f16b5b..867225e 100644 --- a/view/canvas/useCanvasInput.ts +++ b/view/canvas/useCanvasInput.ts @@ -30,6 +30,7 @@ export function useCanvasInput( ): CanvasInputState { const [isPanning, setIsPanning] = useState(false); const brushSession = useRef(); + const brushSessionId = useRef(0); useEffect(() => { const canvas = canvasRef.current; @@ -64,6 +65,7 @@ export function useCanvasInput( const state = store.getState(); const brush = beginBrushSession(state.document, state.editor, viewportPointToDocumentPoint(inputEvent.position, state.editor.viewport)); if (brush) { + brushSessionId.current += 1; brushSession.current = brush; canvas.setPointerCapture(event.pointerId); event.preventDefault(); @@ -99,9 +101,10 @@ export function useCanvasInput( const handlePointerMove = (event: PointerEvent) => { const inputEvent = pointerInputEventFromPointerEvent(event); if (brushSession.current) { + const activeSessionId = brushSessionId.current; const point = viewportPointToDocumentPoint(inputEvent.position, store.getState().editor.viewport); void updateBrushSession({ store, session: brushSession.current, point }).then((nextSession) => { - brushSession.current = nextSession; + if (brushSessionId.current === activeSessionId) brushSession.current = nextSession; }); event.preventDefault(); return; @@ -120,6 +123,7 @@ export function useCanvasInput( const handlePointerUp = (event: PointerEvent) => { const inputEvent = pointerInputEventFromPointerEvent(event); if (brushSession.current) { + brushSessionId.current += 1; brushSession.current = undefined; event.preventDefault(); return;