Advance M8-M11 parity workflows
This commit is contained in:
157
web/protocol/paint-depth-visibility.ts
Normal file
157
web/protocol/paint-depth-visibility.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user