158 lines
7.0 KiB
TypeScript
158 lines
7.0 KiB
TypeScript
import { PAINT_BUDGET } from "./paint";
|
|
|
|
export const PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION = 1 as const;
|
|
export const PAINT_DEPTH_VISIBILITY_BUDGET = {
|
|
maxVertexSamples: PAINT_BUDGET.maxSamples,
|
|
maxReadbackBytes: PAINT_BUDGET.maxStrokeBytes,
|
|
maxDimension: 4096,
|
|
} as const;
|
|
|
|
export type PaintDepthVisibilityBackend = "MAIN_THREAD_WEBGL2" | "OFFSCREEN_WEBGL2";
|
|
|
|
export interface PaintDepthVisibilityRequestIR {
|
|
schemaVersion: typeof PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION;
|
|
objectId: string;
|
|
meshId: string;
|
|
revision: number;
|
|
vertexIndices: number[];
|
|
}
|
|
|
|
export interface PaintDepthVisibilityResultIR extends PaintDepthVisibilityRequestIR {
|
|
backend: PaintDepthVisibilityBackend;
|
|
source: "GPU_RGBA_DEPTH_READBACK";
|
|
width: number;
|
|
height: number;
|
|
depthReadbackBytes: number;
|
|
occluderPixelCount: number;
|
|
visibleVertexIndices: number[];
|
|
}
|
|
|
|
export type PaintDepthVisibilityErrorCode =
|
|
| "PAINT_SCHEMA_INVALID"
|
|
| "PAINT_BUDGET_EXCEEDED"
|
|
| "PAINT_DEPTH_UNAVAILABLE"
|
|
| "REVISION_CONFLICT";
|
|
|
|
export class PaintDepthVisibilityError extends Error {
|
|
constructor(readonly code: PaintDepthVisibilityErrorCode, message: string) {
|
|
super(`${code}: ${message}`);
|
|
this.name = "PaintDepthVisibilityError";
|
|
}
|
|
}
|
|
|
|
const encoder = new TextEncoder();
|
|
const REQUEST_FIELDS = new Set(["schemaVersion", "objectId", "meshId", "revision", "vertexIndices"]);
|
|
const RESULT_FIELDS = new Set([
|
|
...REQUEST_FIELDS,
|
|
"backend",
|
|
"source",
|
|
"width",
|
|
"height",
|
|
"depthReadbackBytes",
|
|
"occluderPixelCount",
|
|
"visibleVertexIndices",
|
|
]);
|
|
|
|
function fail(code: PaintDepthVisibilityErrorCode, message: string): never {
|
|
throw new PaintDepthVisibilityError(code, message);
|
|
}
|
|
|
|
function record(value: unknown, label: string): Record<string, unknown> {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) fail("PAINT_SCHEMA_INVALID", `${label} must be an object`);
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function exact(value: Record<string, unknown>, fields: ReadonlySet<string>, label: string): void {
|
|
if (Object.keys(value).some((field) => !fields.has(field))) fail("PAINT_SCHEMA_INVALID", `${label} contains undeclared fields`);
|
|
}
|
|
|
|
function identity(value: unknown, prefix: "object:" | "mesh:", label: string): string {
|
|
if (typeof value !== "string" || !value.startsWith(prefix) || value.length <= prefix.length || encoder.encode(value).byteLength > 256) {
|
|
fail("PAINT_SCHEMA_INVALID", `${label} is not a bounded ${prefix} identity`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function nonNegativeInteger(value: unknown, label: string): number {
|
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("PAINT_SCHEMA_INVALID", `${label} must be a non-negative safe integer`);
|
|
return value;
|
|
}
|
|
|
|
function vertexIndices(value: unknown, label: string): number[] {
|
|
if (!Array.isArray(value) || value.length === 0) fail("PAINT_SCHEMA_INVALID", `${label} must contain at least one vertex`);
|
|
if (value.length > PAINT_DEPTH_VISIBILITY_BUDGET.maxVertexSamples) fail("PAINT_BUDGET_EXCEEDED", `${label} exceeds the depth sample budget`);
|
|
const parsed = value.map((item, index) => nonNegativeInteger(item, `${label}[${index}]`));
|
|
const unique = new Set(parsed);
|
|
if (unique.size !== parsed.length) fail("PAINT_SCHEMA_INVALID", `${label} contains duplicate vertex identities`);
|
|
return [...unique].sort((left, right) => left - right);
|
|
}
|
|
|
|
function resultVertexIndices(value: unknown): number[] {
|
|
if (!Array.isArray(value)) fail("PAINT_SCHEMA_INVALID", "visibleVertexIndices must be an array");
|
|
if (value.length === 0) return [];
|
|
return vertexIndices(value, "visibleVertexIndices");
|
|
}
|
|
|
|
export function validatePaintDepthVisibilityRequest(value: unknown, currentRevision: number): PaintDepthVisibilityRequestIR {
|
|
const request = record(value, "Paint depth visibility request");
|
|
exact(request, REQUEST_FIELDS, "Paint depth visibility request");
|
|
if (request.schemaVersion !== PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION) fail("PAINT_SCHEMA_INVALID", "Paint depth visibility schema is unsupported");
|
|
const revision = nonNegativeInteger(request.revision, "revision");
|
|
if (!Number.isSafeInteger(currentRevision) || currentRevision < 0) fail("PAINT_SCHEMA_INVALID", "current revision is invalid");
|
|
if (revision !== currentRevision) fail("REVISION_CONFLICT", "Paint depth visibility request is stale");
|
|
return {
|
|
schemaVersion: PAINT_DEPTH_VISIBILITY_SCHEMA_VERSION,
|
|
objectId: identity(request.objectId, "object:", "objectId"),
|
|
meshId: identity(request.meshId, "mesh:", "meshId"),
|
|
revision,
|
|
vertexIndices: vertexIndices(request.vertexIndices, "vertexIndices"),
|
|
};
|
|
}
|
|
|
|
export function validatePaintDepthVisibilityResult(
|
|
value: unknown,
|
|
requestValue: PaintDepthVisibilityRequestIR,
|
|
): PaintDepthVisibilityResultIR {
|
|
const result = record(value, "Paint depth visibility result");
|
|
exact(result, RESULT_FIELDS, "Paint depth visibility result");
|
|
const request = validatePaintDepthVisibilityRequest({
|
|
schemaVersion: result.schemaVersion,
|
|
objectId: result.objectId,
|
|
meshId: result.meshId,
|
|
revision: result.revision,
|
|
vertexIndices: result.vertexIndices,
|
|
}, requestValue.revision);
|
|
if (request.objectId !== requestValue.objectId || request.meshId !== requestValue.meshId ||
|
|
request.vertexIndices.length !== requestValue.vertexIndices.length ||
|
|
request.vertexIndices.some((index, offset) => index !== requestValue.vertexIndices[offset])) {
|
|
fail("PAINT_SCHEMA_INVALID", "Paint depth visibility result does not match its request");
|
|
}
|
|
const backend = result.backend;
|
|
if (backend !== "MAIN_THREAD_WEBGL2" && backend !== "OFFSCREEN_WEBGL2") fail("PAINT_SCHEMA_INVALID", "Paint depth backend is invalid");
|
|
if (result.source !== "GPU_RGBA_DEPTH_READBACK") fail("PAINT_SCHEMA_INVALID", "Paint depth source is invalid");
|
|
const width = nonNegativeInteger(result.width, "width");
|
|
const height = nonNegativeInteger(result.height, "height");
|
|
if (width < 1 || height < 1 || width > PAINT_DEPTH_VISIBILITY_BUDGET.maxDimension || height > PAINT_DEPTH_VISIBILITY_BUDGET.maxDimension) {
|
|
fail("PAINT_BUDGET_EXCEEDED", "Paint depth dimensions exceed the readback budget");
|
|
}
|
|
const depthReadbackBytes = nonNegativeInteger(result.depthReadbackBytes, "depthReadbackBytes");
|
|
if (depthReadbackBytes !== width * height * 4 || depthReadbackBytes > PAINT_DEPTH_VISIBILITY_BUDGET.maxReadbackBytes) {
|
|
fail("PAINT_BUDGET_EXCEEDED", "Paint depth readback byte length is invalid");
|
|
}
|
|
const occluderPixelCount = nonNegativeInteger(result.occluderPixelCount, "occluderPixelCount");
|
|
if (occluderPixelCount > width * height) fail("PAINT_SCHEMA_INVALID", "Paint depth occluder count exceeds the target");
|
|
const visibleVertexIndices = resultVertexIndices(result.visibleVertexIndices);
|
|
const requested = new Set(request.vertexIndices);
|
|
if (visibleVertexIndices.some((index) => !requested.has(index))) fail("PAINT_SCHEMA_INVALID", "Paint depth result contains an unrequested vertex");
|
|
return {
|
|
...request,
|
|
backend,
|
|
source: "GPU_RGBA_DEPTH_READBACK",
|
|
width,
|
|
height,
|
|
depthReadbackBytes,
|
|
occluderPixelCount,
|
|
visibleVertexIndices,
|
|
};
|
|
}
|