feat(renderer): enhance image rendering with asset synchronization and clipping

This commit is contained in:
syntaxbullet
2026-07-03 16:34:31 +02:00
parent bc0fbea029
commit ba3f253ee5
7 changed files with 158 additions and 28 deletions

View File

@@ -2,7 +2,8 @@ import type { Asset } from "@core/asset";
import type { ScreenRect, WebGlRendererContext } from "./types";
export type ImageTextureRenderer = {
render(asset: Asset, rect: ScreenRect): boolean;
syncAssets(assets: readonly Asset[]): void;
render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect): boolean;
dispose(): void;
};
@@ -20,6 +21,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
const positionBuffer = gl.createBuffer();
const texCoordBuffer = gl.createBuffer();
const textures = new Map<string, TextureEntry>();
let disposed = false;
if (!positionBuffer || !texCoordBuffer || !samplerLocation) throw new Error("Failed to create image texture renderer");
@@ -27,11 +29,24 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW);
return {
render(asset, rect) {
const entry = getTextureEntry(context, textures, asset, invalidate);
syncAssets(assets) {
const activeKeys = new Set(assets.map(textureKey));
for (const [key, entry] of textures) {
if (!activeKeys.has(key)) {
disposeEntry(gl, entry);
textures.delete(key);
}
}
},
render(asset, rect, clipRect) {
const drawRect = clipRect ? intersectScreenRects(rect, clipRect) : rect;
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed);
if (entry.status !== "ready") return false;
gl.disable(gl.SCISSOR_TEST);
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.useProgram(program);
@@ -51,13 +66,11 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disable(gl.BLEND);
gl.enable(gl.SCISSOR_TEST);
return true;
},
dispose() {
for (const entry of textures.values()) {
if (entry.status === "ready") gl.deleteTexture(entry.texture);
}
disposed = true;
for (const entry of textures.values()) disposeEntry(gl, entry);
gl.deleteBuffer(positionBuffer);
gl.deleteBuffer(texCoordBuffer);
gl.deleteProgram(program);
@@ -70,19 +83,24 @@ function getTextureEntry(
textures: Map<string, TextureEntry>,
asset: Asset,
invalidate: () => void,
isDisposed: () => boolean,
): TextureEntry {
const cached = textures.get(asset.id);
const key = textureKey(asset);
const cached = textures.get(key);
if (cached) return cached;
const image = new Image();
textures.set(asset.id, { status: "loading", image });
textures.set(key, { status: "loading", image });
image.onload = () => {
if (isDisposed()) return;
const texture = createTexture(context.gl, image);
textures.set(asset.id, { status: "ready", texture });
disposeEntry(context.gl, textures.get(key));
textures.set(key, { status: "ready", texture });
invalidate();
};
image.onerror = () => {
textures.set(asset.id, { status: "error" });
if (isDisposed()) return;
textures.set(key, { status: "error" });
invalidate();
};
image.src = asset.source;
@@ -90,6 +108,32 @@ function getTextureEntry(
return { status: "loading", image };
}
function textureKey(asset: Asset) {
return `${asset.id}:${asset.source}`;
}
function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry | undefined) {
if (!entry) return;
if (entry.status === "ready") {
gl.deleteTexture(entry.texture);
return;
}
if (entry.status === "loading") {
entry.image.onload = null;
entry.image.onerror = null;
}
}
function intersectScreenRects(a: ScreenRect, b: ScreenRect): ScreenRect | undefined {
const x1 = Math.max(a.x, b.x);
const y1 = Math.max(a.y, b.y);
const x2 = Math.min(a.x + a.w, b.x + b.w);
const y2 = Math.min(a.y + a.h, b.y + b.h);
if (x2 <= x1 || y2 <= y1) return undefined;
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
}
function createTexture(gl: WebGL2RenderingContext, image: HTMLImageElement) {
const texture = gl.createTexture();
if (!texture) throw new Error("Failed to create image texture");