Use the gpu for drawing meshes only when it has more than 16 triangles (bug 2030745)

And in order to slightly improve performances, move the figure creation in the worker.
This commit is contained in:
Calixte Denizet 2026-04-13 21:05:00 +02:00
parent 96debf0c81
commit a2c57ee69e
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
7 changed files with 151 additions and 365 deletions

View File

@ -265,7 +265,6 @@ function compilePatternInfo(ir) {
coords = [], coords = [],
colors = [], colors = [],
colorStops = [], colorStops = [],
figures = [],
shadingType = null, // only needed for mesh patterns shadingType = null, // only needed for mesh patterns
background = null; // background for mesh patterns background = null; // background for mesh patterns
@ -285,7 +284,6 @@ function compilePatternInfo(ir) {
shadingType = ir[1]; shadingType = ir[1];
coords = ir[2]; coords = ir[2];
colors = ir[3]; colors = ir[3];
figures = ir[4] || [];
bbox = ir[6]; bbox = ir[6];
background = ir[7]; background = ir[7];
break; break;
@ -296,18 +294,6 @@ function compilePatternInfo(ir) {
const nCoord = Math.floor(coords.length / 2); const nCoord = Math.floor(coords.length / 2);
const nColor = Math.floor(colors.length / 4); const nColor = Math.floor(colors.length / 4);
const nStop = colorStops.length; const nStop = colorStops.length;
const nFigures = figures.length;
let figuresSize = 0;
for (const figure of figures) {
figuresSize += 1;
figuresSize = Math.ceil(figuresSize / 4) * 4; // Ensure 4-byte alignment
figuresSize += 4 + figure.coords.length * 4;
figuresSize += 4 + figure.colors.length * 4;
if (figure.verticesPerRow !== undefined) {
figuresSize += 4;
}
}
const byteLen = const byteLen =
20 + 20 +
@ -315,8 +301,7 @@ function compilePatternInfo(ir) {
nColor * 4 + nColor * 4 +
nStop * 8 + nStop * 8 +
(bbox ? 16 : 0) + (bbox ? 16 : 0) +
(background ? 3 : 0) + (background ? 3 : 0);
figuresSize;
const buffer = new ArrayBuffer(byteLen); const buffer = new ArrayBuffer(byteLen);
const dataView = new DataView(buffer); const dataView = new DataView(buffer);
const u8data = new Uint8Array(buffer); const u8data = new Uint8Array(buffer);
@ -328,7 +313,7 @@ function compilePatternInfo(ir) {
dataView.setUint32(PATTERN_INFO.N_COORD, nCoord, true); dataView.setUint32(PATTERN_INFO.N_COORD, nCoord, true);
dataView.setUint32(PATTERN_INFO.N_COLOR, nColor, true); dataView.setUint32(PATTERN_INFO.N_COLOR, nColor, true);
dataView.setUint32(PATTERN_INFO.N_STOP, nStop, true); dataView.setUint32(PATTERN_INFO.N_STOP, nStop, true);
dataView.setUint32(PATTERN_INFO.N_FIGURES, nFigures, true); dataView.setUint32(PATTERN_INFO.N_FIGURES, 0, true);
let offset = 20; let offset = 20;
const coordsView = new Float32Array(buffer, offset, nCoord * 2); const coordsView = new Float32Array(buffer, offset, nCoord * 2);
@ -353,34 +338,6 @@ function compilePatternInfo(ir) {
if (background) { if (background) {
u8data.set(background, offset); u8data.set(background, offset);
offset += 3;
}
for (let i = 0; i < figures.length; i++) {
const figure = figures[i];
dataView.setUint8(offset, figure.type);
offset += 1;
// Ensure 4-byte alignment
offset = Math.ceil(offset / 4) * 4;
dataView.setUint32(offset, figure.coords.length, true);
offset += 4;
const figureCoordsView = new Int32Array(
buffer,
offset,
figure.coords.length
);
figureCoordsView.set(figure.coords);
offset += figure.coords.length * 4;
dataView.setUint32(offset, figure.colors.length, true);
offset += 4;
const colorsView = new Int32Array(buffer, offset, figure.colors.length);
colorsView.set(figure.colors);
offset += figure.colors.length * 4;
if (figure.verticesPerRow !== undefined) {
dataView.setUint32(offset, figure.verticesPerRow, true);
offset += 4;
}
} }
return buffer; return buffer;
} }

View File

@ -379,6 +379,63 @@ function meshPackData(self) {
} }
} }
function buildMeshVertexData(coords, colors, figures) {
// Count the total expanded vertex count first for a single allocation.
let vertexCount = 0;
for (const figure of figures) {
if (figure.type === MeshFigureType.TRIANGLES) {
vertexCount += figure.coords.length;
} else if (figure.type === MeshFigureType.LATTICE) {
const vpr = figure.verticesPerRow;
vertexCount +=
(Math.floor(figure.coords.length / vpr) - 1) * (vpr - 1) * 6;
}
}
// posData: 2 × float32 per vertex (raw PDF content-space x, y).
// colData: 4 × uint8 per vertex (r, g, b, unused).
const posData = new Float32Array(vertexCount * 2);
const colData = new Uint8Array(vertexCount * 4);
let pOff = 0,
cOff = 0;
const addVertex = (pi, ci) => {
posData[pOff++] = coords[pi * 2];
posData[pOff++] = coords[pi * 2 + 1];
colData[cOff++] = colors[ci * 4];
colData[cOff++] = colors[ci * 4 + 1];
colData[cOff++] = colors[ci * 4 + 2];
cOff++; // alpha padding
};
for (const figure of figures) {
const ps = figure.coords;
const cs = figure.colors;
if (figure.type === MeshFigureType.TRIANGLES) {
for (let i = 0, ii = ps.length; i < ii; i++) {
addVertex(ps[i], cs[i]);
}
} else if (figure.type === MeshFigureType.LATTICE) {
const vpr = figure.verticesPerRow;
const rows = Math.floor(ps.length / vpr) - 1;
const cols = vpr - 1;
for (let i = 0; i < rows; i++) {
let q = i * vpr;
for (let j = 0; j < cols; j++, q++) {
addVertex(ps[q], cs[q]);
addVertex(ps[q + 1], cs[q + 1]);
addVertex(ps[q + vpr], cs[q + vpr]);
addVertex(ps[q + vpr + 1], cs[q + vpr + 1]);
addVertex(ps[q + 1], cs[q + 1]);
addVertex(ps[q + vpr], cs[q + vpr]);
}
}
}
}
return { posData, colData, vertexCount };
}
// Type 1 shading: a 2-in, n-out function sampled over a rectangular domain. // Type 1 shading: a 2-in, n-out function sampled over a rectangular domain.
class FunctionBasedShading extends BaseShading { class FunctionBasedShading extends BaseShading {
// Maximum grid steps per axis to avoid huge meshes. // Maximum grid steps per axis to avoid huge meshes.
@ -485,12 +542,17 @@ class FunctionBasedShading extends BaseShading {
} }
getIR() { getIR() {
const { posData, colData, vertexCount } = buildMeshVertexData(
this.coords,
this.colors,
this.figures
);
return [ return [
"Mesh", "Mesh",
ShadingType.FUNCTION_BASED, ShadingType.FUNCTION_BASED,
this.coords, posData,
this.colors, colData,
this.figures, vertexCount,
this.bounds, this.bounds,
this.bbox, this.bbox,
this.background, this.background,
@ -1114,12 +1176,17 @@ class MeshShading extends BaseShading {
} }
getIR() { getIR() {
const { posData, colData, vertexCount } = buildMeshVertexData(
this.coords,
this.colors,
this.figures
);
return [ return [
"Mesh", "Mesh",
this.shadingType, this.shadingType,
this.coords, posData,
this.colors, colData,
this.figures, vertexCount,
this.bounds, this.bounds,
this.bbox, this.bbox,
this.background, this.background,

View File

@ -13,13 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { assert, BBOX_INIT, FeatureTest, Util } from "../shared/util.js";
assert,
BBOX_INIT,
FeatureTest,
MeshFigureType,
Util,
} from "../shared/util.js";
import { import {
CSS_FONT_INFO, CSS_FONT_INFO,
FONT_INFO, FONT_INFO,
@ -354,7 +348,6 @@ class PatternInfo {
const nCoord = dataView.getUint32(PATTERN_INFO.N_COORD, true); const nCoord = dataView.getUint32(PATTERN_INFO.N_COORD, true);
const nColor = dataView.getUint32(PATTERN_INFO.N_COLOR, true); const nColor = dataView.getUint32(PATTERN_INFO.N_COLOR, true);
const nStop = dataView.getUint32(PATTERN_INFO.N_STOP, true); const nStop = dataView.getUint32(PATTERN_INFO.N_STOP, true);
const nFigures = dataView.getUint32(PATTERN_INFO.N_FIGURES, true);
let offset = 20; let offset = 20;
const coords = new Float32Array(this.buffer, offset, nCoord * 2); const coords = new Float32Array(this.buffer, offset, nCoord * 2);
@ -384,37 +377,6 @@ class PatternInfo {
offset += 3; offset += 3;
} }
const figures = [];
for (let i = 0; i < nFigures; ++i) {
const type = dataView.getUint8(offset);
offset += 1;
// Ensure 4-byte alignment
offset = Math.ceil(offset / 4) * 4;
const coordsLength = dataView.getUint32(offset, true);
offset += 4;
const figureCoords = new Int32Array(this.buffer, offset, coordsLength);
offset += coordsLength * 4;
const colorsLength = dataView.getUint32(offset, true);
offset += 4;
const figureColors = new Int32Array(this.buffer, offset, colorsLength);
offset += colorsLength * 4;
const figure = {
type,
coords: figureCoords,
colors: figureColors,
};
if (type === MeshFigureType.LATTICE) {
figure.verticesPerRow = dataView.getUint32(offset, true);
offset += 4;
}
figures.push(figure);
}
if (kind === 1) { if (kind === 1) {
// axial // axial
return [ return [
@ -455,7 +417,7 @@ class PatternInfo {
shadingType, shadingType,
coords, coords,
colors, colors,
figures, nCoord,
bounds, bounds,
bbox, bbox,
background, background,

View File

@ -14,13 +14,7 @@
*/ */
import { drawMeshWithGPU, isGPUReady, loadMeshShader } from "./webgpu.js"; import { drawMeshWithGPU, isGPUReady, loadMeshShader } from "./webgpu.js";
import { import { FormatError, info, unreachable, Util } from "../shared/util.js";
FormatError,
info,
MeshFigureType,
unreachable,
Util,
} from "../shared/util.js";
import { getCurrentTransform } from "./display_utils.js"; import { getCurrentTransform } from "./display_utils.js";
const PathType = { const PathType = {
@ -377,66 +371,12 @@ function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
} }
} }
function drawFigure(data, figure, context) {
const ps = figure.coords;
const cs = figure.colors;
let i, ii;
switch (figure.type) {
case MeshFigureType.LATTICE:
const verticesPerRow = figure.verticesPerRow;
const rows = Math.floor(ps.length / verticesPerRow) - 1;
const cols = verticesPerRow - 1;
for (i = 0; i < rows; i++) {
let q = i * verticesPerRow;
for (let j = 0; j < cols; j++, q++) {
drawTriangle(
data,
context,
ps[q],
ps[q + 1],
ps[q + verticesPerRow],
cs[q],
cs[q + 1],
cs[q + verticesPerRow]
);
drawTriangle(
data,
context,
ps[q + verticesPerRow + 1],
ps[q + 1],
ps[q + verticesPerRow],
cs[q + verticesPerRow + 1],
cs[q + 1],
cs[q + verticesPerRow]
);
}
}
break;
case MeshFigureType.TRIANGLES:
for (i = 0, ii = ps.length; i < ii; i += 3) {
drawTriangle(
data,
context,
ps[i],
ps[i + 1],
ps[i + 2],
cs[i],
cs[i + 1],
cs[i + 2]
);
}
break;
default:
throw new Error("illegal figure");
}
}
class MeshShadingPattern extends BaseShadingPattern { class MeshShadingPattern extends BaseShadingPattern {
constructor(IR) { constructor(IR) {
super(); super();
this._coords = IR[2]; this._posData = IR[2];
this._colors = IR[3]; this._colData = IR[3];
this._figures = IR[4]; this._vertexCount = IR[4];
this._bounds = IR[5]; this._bounds = IR[5];
this._bbox = IR[6]; this._bbox = IR[6];
this._background = IR[7]; this._background = IR[7];
@ -477,8 +417,8 @@ class MeshShadingPattern extends BaseShadingPattern {
const scaleY = boundsHeight ? boundsHeight / height : 1; const scaleY = boundsHeight ? boundsHeight / height : 1;
const context = { const context = {
coords: this._coords, coords: this._posData,
colors: this._colors, colors: this._colData,
offsetX: -offsetX, offsetX: -offsetX,
offsetY: -offsetY, offsetY: -offsetY,
scaleX: 1 / scaleX, scaleX: 1 / scaleX,
@ -489,10 +429,17 @@ class MeshShadingPattern extends BaseShadingPattern {
const paddedHeight = height + BORDER_SIZE * 2; const paddedHeight = height + BORDER_SIZE * 2;
const tmpCanvas = canvasFactory.create(paddedWidth, paddedHeight); const tmpCanvas = canvasFactory.create(paddedWidth, paddedHeight);
if (isGPUReady()) { // Use the GPU path when there are more than 16 triangles (> 48 vertices).
// With small meshes the GPU overhead is significant and the CPU path is
// faster. The texture has to move from the GPU to the main thread and it's
// costly. So it's frequent to have a lot of mesh-based shading patterns
// when rendering some 3D surfaces (see bug 2030745).
if (isGPUReady() && this._vertexCount > 48) {
tmpCanvas.context.drawImage( tmpCanvas.context.drawImage(
drawMeshWithGPU( drawMeshWithGPU(
this._figures, this._posData,
this._colData,
this._vertexCount,
context, context,
backgroundColor, backgroundColor,
paddedWidth, paddedWidth,
@ -513,8 +460,8 @@ class MeshShadingPattern extends BaseShadingPattern {
bytes[i + 3] = 255; bytes[i + 3] = 255;
} }
} }
for (const figure of this._figures) { for (let i = 0, ii = this._vertexCount; i < ii; i += 3) {
drawFigure(data, figure, context); drawTriangle(data, context, i, i + 1, i + 2, i, i + 1, i + 2);
} }
tmpCanvas.context.putImageData(data, BORDER_SIZE, BORDER_SIZE); tmpCanvas.context.putImageData(data, BORDER_SIZE, BORDER_SIZE);
} }

View File

@ -13,8 +13,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { MeshFigureType } from "../shared/util.js";
// WGSL shader for Gouraud-shaded triangle mesh rasterization. // WGSL shader for Gouraud-shaded triangle mesh rasterization.
// Vertices arrive in PDF content-space coordinates; the vertex shader // Vertices arrive in PDF content-space coordinates; the vertex shader
// applies the affine transform supplied via a uniform buffer to map them // applies the affine transform supplied via a uniform buffer to map them
@ -141,94 +139,13 @@ class WebGPU {
}); });
} }
/**
* Build flat Float32Array (positions) and Uint8Array (colors) vertex
* streams for non-indexed triangle-list rendering.
*
* Coords and colors intentionally use separate lookup indices. For patch
* mesh figures (types 6/7 converted to LATTICE in the worker), the coord
* index-space and color index-space differ because the stream interleaves
* them at different densities (12 coords but 4 colors per flag-0 patch).
* A single shared index buffer cannot represent both simultaneously, so we
* expand each triangle vertex individually into the two flat streams.
*
* @param {Array} figures
* @param {Object} context coords/colors/offsetX/offsetY/scaleX/scaleY
* @returns {{ posData: Float32Array, colData: Uint8Array,
* vertexCount: number }}
*/
#buildVertexStreams(figures, context) {
const { coords, colors } = context;
// Count vertices first so we can allocate the typed arrays exactly once.
let vertexCount = 0;
for (const figure of figures) {
const ps = figure.coords;
if (figure.type === MeshFigureType.TRIANGLES) {
vertexCount += ps.length;
} else if (figure.type === MeshFigureType.LATTICE) {
const vpr = figure.verticesPerRow;
// 2 triangles × 3 vertices per quad cell
vertexCount += (Math.floor(ps.length / vpr) - 1) * (vpr - 1) * 6;
}
}
// posData: 2 × float32 per vertex (raw PDF content-space x, y).
// colData: 4 × uint8 per vertex (r, g, b, unused — required by unorm8x4).
const posData = new Float32Array(vertexCount * 2);
const colData = new Uint8Array(vertexCount * 4);
let pOff = 0,
cOff = 0;
// pi and ci are raw vertex indices; coords is stride-2, colors stride-4.
const addVertex = (pi, ci) => {
posData[pOff++] = coords[pi * 2];
posData[pOff++] = coords[pi * 2 + 1];
colData[cOff++] = colors[ci * 4];
colData[cOff++] = colors[ci * 4 + 1];
colData[cOff++] = colors[ci * 4 + 2];
cOff++; // alpha channel — unused in the fragment shader
};
for (const figure of figures) {
const ps = figure.coords;
const cs = figure.colors;
if (figure.type === MeshFigureType.TRIANGLES) {
for (let i = 0, ii = ps.length; i < ii; i += 3) {
addVertex(ps[i], cs[i]);
addVertex(ps[i + 1], cs[i + 1]);
addVertex(ps[i + 2], cs[i + 2]);
}
} else if (figure.type === MeshFigureType.LATTICE) {
const vpr = figure.verticesPerRow;
const rows = Math.floor(ps.length / vpr) - 1;
const cols = vpr - 1;
for (let i = 0; i < rows; i++) {
let q = i * vpr;
for (let j = 0; j < cols; j++, q++) {
// Upper-left triangle: q, q+1, q+vpr
addVertex(ps[q], cs[q]);
addVertex(ps[q + 1], cs[q + 1]);
addVertex(ps[q + vpr], cs[q + vpr]);
// Lower-right triangle: q+vpr+1, q+1, q+vpr
addVertex(ps[q + vpr + 1], cs[q + vpr + 1]);
addVertex(ps[q + 1], cs[q + 1]);
addVertex(ps[q + vpr], cs[q + vpr]);
}
}
}
}
return { posData, colData, vertexCount };
}
/** /**
* Render a mesh shading to an ImageBitmap using WebGPU. * Render a mesh shading to an ImageBitmap using WebGPU.
* *
* Two flat vertex streams (positions and colors) are uploaded from the * The flat vertex streams (positions and colors) were pre-built by the
* packed IR typed arrays. A uniform buffer carries the affine transform * worker and arrive ready to upload. A uniform buffer carries the affine
* so the vertex shader maps PDF content-space coordinates to NDC without * transform so the vertex shader maps PDF content-space coordinates to NDC
* any CPU arithmetic per vertex. * without any CPU arithmetic per vertex.
* *
* After `device.queue.submit()`, `transferToImageBitmap()` presents the * After `device.queue.submit()`, `transferToImageBitmap()` presents the
* current GPU frame synchronously the browser ensures all submitted GPU * current GPU frame synchronously the browser ensures all submitted GPU
@ -237,8 +154,10 @@ class WebGPU {
* *
* The GPU device must already be initialized (`this.isReady === true`). * The GPU device must already be initialized (`this.isReady === true`).
* *
* @param {Array} figures * @param {Float32Array} posData flat vertex positions (x,y per vertex)
* @param {Object} context coords/colors/offsetX/offsetY/ * @param {Uint8Array} colData flat vertex colors (r,g,b,_ per vertex)
* @param {number} vertexCount
* @param {Object} context offsetX/offsetY/scaleX/scaleY
* @param {Uint8Array|null} backgroundColor [r,g,b] or null for transparent * @param {Uint8Array|null} backgroundColor [r,g,b] or null for transparent
* @param {number} paddedWidth render-target width * @param {number} paddedWidth render-target width
* @param {number} paddedHeight render-target height * @param {number} paddedHeight render-target height
@ -246,7 +165,9 @@ class WebGPU {
* @returns {ImageBitmap} * @returns {ImageBitmap}
*/ */
draw( draw(
figures, posData,
colData,
vertexCount,
context, context,
backgroundColor, backgroundColor,
paddedWidth, paddedWidth,
@ -258,10 +179,6 @@ class WebGPU {
const device = this.#device; const device = this.#device;
const { offsetX, offsetY, scaleX, scaleY } = context; const { offsetX, offsetY, scaleX, scaleY } = context;
const { posData, colData, vertexCount } = this.#buildVertexStreams(
figures,
context
);
// Upload vertex positions (raw PDF coords) and colors as separate buffers. // Upload vertex positions (raw PDF coords) and colors as separate buffers.
// GPUBufferUsage requires size > 0. // GPUBufferUsage requires size > 0.
@ -381,7 +298,9 @@ function loadMeshShader() {
} }
function drawMeshWithGPU( function drawMeshWithGPU(
figures, posData,
colData,
vertexCount,
context, context,
backgroundColor, backgroundColor,
paddedWidth, paddedWidth,
@ -389,7 +308,9 @@ function drawMeshWithGPU(
borderSize borderSize
) { ) {
return _webGPU.draw( return _webGPU.draw(
figures, posData,
colData,
vertexCount,
context, context,
backgroundColor, backgroundColor,
paddedWidth, paddedWidth,

View File

@ -27,7 +27,7 @@ import {
PatternInfo, PatternInfo,
SystemFontInfo, SystemFontInfo,
} from "../../src/display/obj_bin_transform_display.js"; } from "../../src/display/obj_bin_transform_display.js";
import { FeatureTest, MeshFigureType } from "../../src/shared/util.js"; import { FeatureTest } from "../../src/shared/util.js";
describe("obj_bin_transform", function () { describe("obj_bin_transform", function () {
describe("Font data", function () { describe("Font data", function () {
@ -208,6 +208,8 @@ describe("obj_bin_transform", function () {
25, 25,
]; ];
// Vertices are pre-expanded in the new IR format: posData/colData contain
// one entry per vertex (no indexing), and ir[4] is the vertex count.
const meshPatternIR = [ const meshPatternIR = [
"Mesh", "Mesh",
4, 4,
@ -218,19 +220,7 @@ describe("obj_bin_transform", function () {
255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 255, 255, 0, 0, 128, 128, 128, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0, 255, 255, 0, 0, 128, 128, 128,
0, 255, 0, 255, 0, 0, 255, 255, 0, 255, 128, 0, 0, 128, 0, 128, 0, 0, 255, 0, 255, 0, 0, 255, 255, 0, 255, 128, 0, 0, 128, 0, 128, 0,
]), ]),
[ 9, // vertexCount (3 triangles × 3 vertices)
{
type: MeshFigureType.TRIANGLES,
coords: new Int32Array([0, 2, 4, 6, 8, 10, 12, 14, 16]),
colors: new Int32Array([0, 2, 4, 6, 8, 10, 12, 14, 16]),
},
{
type: MeshFigureType.LATTICE,
coords: new Int32Array([0, 2, 4, 6, 8, 10]),
colors: new Int32Array([0, 2, 4, 6, 8, 10]),
verticesPerRow: 3,
},
],
[0, 0, 100, 100], [0, 0, 100, 100],
[0, 0, 100, 100], [0, 0, 100, 100],
[128, 128, 128], [128, 128, 128],
@ -302,27 +292,7 @@ describe("obj_bin_transform", function () {
expect(Array.from(reconstructedIR[3])).toEqual( expect(Array.from(reconstructedIR[3])).toEqual(
Array.from(meshPatternIR[3]) Array.from(meshPatternIR[3])
); );
expect(reconstructedIR[4].length).toEqual(2); expect(reconstructedIR[4]).toEqual(9); // vertexCount
const fig1 = reconstructedIR[4][0];
expect(fig1.type).toEqual(MeshFigureType.TRIANGLES);
expect(fig1.coords).toBeInstanceOf(Int32Array);
expect(Array.from(fig1.coords)).toEqual([
0, 2, 4, 6, 8, 10, 12, 14, 16,
]);
expect(fig1.colors).toBeInstanceOf(Int32Array);
expect(Array.from(fig1.colors)).toEqual([
0, 2, 4, 6, 8, 10, 12, 14, 16,
]);
expect(fig1.verticesPerRow).toBeUndefined();
const fig2 = reconstructedIR[4][1];
expect(fig2.type).toEqual(MeshFigureType.LATTICE);
expect(fig2.coords).toBeInstanceOf(Int32Array);
expect(Array.from(fig2.coords)).toEqual([0, 2, 4, 6, 8, 10]);
expect(fig2.colors).toBeInstanceOf(Int32Array);
expect(Array.from(fig2.colors)).toEqual([0, 2, 4, 6, 8, 10]);
expect(fig2.verticesPerRow).toEqual(3);
expect(reconstructedIR[5]).toEqual([0, 0, 100, 100]); expect(reconstructedIR[5]).toEqual([0, 0, 100, 100]);
expect(reconstructedIR[6]).toEqual([0, 0, 100, 100]); expect(reconstructedIR[6]).toEqual([0, 0, 100, 100]);
@ -330,42 +300,38 @@ describe("obj_bin_transform", function () {
expect(Array.from(reconstructedIR[7])).toEqual([128, 128, 128]); expect(Array.from(reconstructedIR[7])).toEqual([128, 128, 128]);
}); });
it("must handle mesh patterns with no figures", function () { it("must handle mesh patterns with no vertices", function () {
const noFiguresIR = [ const noVerticesIR = [
"Mesh", "Mesh",
4, 4,
new Float32Array([0, 0, 10, 10]), new Float32Array([0, 0, 10, 10]),
new Uint8Array([255, 0, 0, 0]), new Uint8Array([255, 0, 0, 0]),
[], 2, // vertexCount
[0, 0, 10, 10], [0, 0, 10, 10],
[0, 0, 10, 10], [0, 0, 10, 10],
null, null,
]; ];
const buffer = compilePatternInfo(noFiguresIR); const buffer = compilePatternInfo(noVerticesIR);
const patternInfo = new PatternInfo(buffer); const patternInfo = new PatternInfo(buffer);
const reconstructedIR = patternInfo.getIR(); const reconstructedIR = patternInfo.getIR();
expect(reconstructedIR[4]).toEqual([]); expect(reconstructedIR[4]).toEqual(2); // vertexCount
expect(reconstructedIR[7]).toBeNull(); // background should be null expect(reconstructedIR[7]).toBeNull(); // background should be null
}); });
it("must preserve figure data integrity across serialization", function () { it("must preserve vertex data integrity across serialization", function () {
const buffer = compilePatternInfo(meshPatternIR); const buffer = compilePatternInfo(meshPatternIR);
const patternInfo = new PatternInfo(buffer); const patternInfo = new PatternInfo(buffer);
const reconstructedIR = patternInfo.getIR(); const reconstructedIR = patternInfo.getIR();
// Verify data integrity by checking exact values // Verify posData and colData are preserved exactly
const originalFig = meshPatternIR[4][0]; expect(Array.from(reconstructedIR[2])).toEqual(
const reconstructedFig = reconstructedIR[4][0]; Array.from(meshPatternIR[2])
);
for (let i = 0; i < originalFig.coords.length; i++) { expect(Array.from(reconstructedIR[3])).toEqual(
expect(reconstructedFig.coords[i]).toEqual(originalFig.coords[i]); Array.from(meshPatternIR[3])
} );
for (let i = 0; i < originalFig.colors.length; i++) {
expect(reconstructedFig.colors[i]).toEqual(originalFig.colors[i]);
}
}); });
it("must calculate correct buffer sizes for different pattern types", function () { it("must calculate correct buffer sizes for different pattern types", function () {
@ -378,36 +344,25 @@ describe("obj_bin_transform", function () {
expect(meshBuffer.byteLength).toBeGreaterThan(radialBuffer.byteLength); expect(meshBuffer.byteLength).toBeGreaterThan(radialBuffer.byteLength);
}); });
it("must handle figures with different type enums correctly", function () { it("must round-trip mesh pattern posData and colData correctly", function () {
const customFiguresIR = [ const customMeshIR = [
"Mesh", "Mesh",
6, 6,
new Float32Array([0, 0, 10, 10]), new Float32Array([0, 0, 10, 10]),
new Uint8Array([255, 128, 64, 0]), new Uint8Array([255, 128, 64, 0]),
[ 2, // vertexCount
{
type: MeshFigureType.PATCH,
coords: new Int32Array([0, 2]),
colors: new Int32Array([0, 2]),
},
{
type: MeshFigureType.TRIANGLES,
coords: new Int32Array([0]),
colors: new Int32Array([0]),
},
],
[0, 0, 10, 10], [0, 0, 10, 10],
null, null,
null, null,
]; ];
const buffer = compilePatternInfo(customFiguresIR); const buffer = compilePatternInfo(customMeshIR);
const patternInfo = new PatternInfo(buffer); const patternInfo = new PatternInfo(buffer);
const reconstructedIR = patternInfo.getIR(); const reconstructedIR = patternInfo.getIR();
expect(reconstructedIR[4].length).toEqual(2); expect(reconstructedIR[4]).toEqual(2); // vertexCount
expect(reconstructedIR[4][0].type).toEqual(MeshFigureType.PATCH); expect(Array.from(reconstructedIR[2])).toEqual([0, 0, 10, 10]);
expect(reconstructedIR[4][1].type).toEqual(MeshFigureType.TRIANGLES); expect(Array.from(reconstructedIR[3])).toEqual([255, 128, 64, 0]);
}); });
it("must handle mesh patterns with different background values", function () { it("must handle mesh patterns with different background values", function () {
@ -416,7 +371,7 @@ describe("obj_bin_transform", function () {
4, 4,
new Float32Array([0, 0, 10, 10]), new Float32Array([0, 0, 10, 10]),
new Uint8Array([255, 0, 0, 0]), new Uint8Array([255, 0, 0, 0]),
[], 2, // vertexCount
[0, 0, 10, 10], [0, 0, 10, 10],
[0, 0, 10, 10], [0, 0, 10, 10],
new Uint8Array([255, 128, 64]), new Uint8Array([255, 128, 64]),
@ -433,7 +388,7 @@ describe("obj_bin_transform", function () {
5, 5,
new Float32Array([0, 0, 5, 5]), new Float32Array([0, 0, 5, 5]),
new Uint8Array([0, 255, 0, 0]), new Uint8Array([0, 255, 0, 0]),
[], 2, // vertexCount
[0, 0, 5, 5], [0, 0, 5, 5],
null, null,
null, null,
@ -452,7 +407,7 @@ describe("obj_bin_transform", function () {
4, 4,
new Float32Array([-10, -5, 20, 15, 0, 30]), new Float32Array([-10, -5, 20, 15, 0, 30]),
new Uint8Array([255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0]), new Uint8Array([255, 0, 0, 0, 0, 255, 0, 0, 0, 0, 255, 0]),
[], 3, // vertexCount
null, null,
null, null,
null, null,

View File

@ -74,33 +74,13 @@ describe("pattern", function () {
expect(ir[0]).toEqual("Mesh"); expect(ir[0]).toEqual("Mesh");
expect(ir[1]).toEqual(1); expect(ir[1]).toEqual(1);
// Vertices are pre-expanded: 3×4 lattice →
// 6 quads → 12 triangles → 36 vertices
expect(ir[2]).toBeInstanceOf(Float32Array); expect(ir[2]).toBeInstanceOf(Float32Array);
expect(ir[2].length).toEqual(24); expect(ir[2].length).toEqual(72); // 36 vertices × 2 coords
expect(Array.from(ir[2].slice(0, 6))).toEqual([10, 20, 11, 20, 12, 20]); expect(ir[3]).toBeInstanceOf(Uint8Array);
expect(Array.from(ir[2].slice(-6))).toEqual([10, 23, 11, 23, 12, 23]); expect(ir[3].length).toEqual(144); // 36 vertices × 4 bytes
expect(ir[4]).toEqual(36); // vertexCount
expect(ir[3]).toBeInstanceOf(Uint8ClampedArray);
expect(ir[3].length).toEqual(48);
expect(Array.from(ir[3].slice(0, 12))).toEqual([
0, 0, 0, 0, 128, 0, 0, 0, 191, 0, 0, 0,
]);
expect(Array.from(ir[3].slice(-12))).toEqual([
0, 212, 0, 0, 128, 212, 0, 0, 191, 212, 0, 0,
]);
expect(ir[4]).toEqual([
jasmine.objectContaining({
verticesPerRow: 3,
}),
]);
expect(ir[4][0].coords).toBeInstanceOf(Uint32Array);
expect(Array.from(ir[4][0].coords)).toEqual([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
]);
expect(ir[4][0].colors).toBeInstanceOf(Uint32Array);
expect(Array.from(ir[4][0].colors)).toEqual([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
]);
expect(ir[5]).toEqual([10, 20, 12, 23]); expect(ir[5]).toEqual([10, 20, 12, 23]);
expect(ir[6]).toBeNull(); expect(ir[6]).toBeNull();
expect(ir[7]).toBeNull(); expect(ir[7]).toBeNull();
@ -110,17 +90,14 @@ describe("pattern", function () {
const shading = createFunctionBasedShading({ const shading = createFunctionBasedShading({
background: [0.25, 0.5, 0.75], background: [0.25, 0.5, 0.75],
}); });
const buffer = compilePatternInfo(shading.getIR()); const ir = shading.getIR();
const buffer = compilePatternInfo(ir);
const reconstructedIR = new PatternInfo(buffer).getIR(); const reconstructedIR = new PatternInfo(buffer).getIR();
expect(reconstructedIR[0]).toEqual("Mesh"); expect(reconstructedIR[0]).toEqual("Mesh");
expect(reconstructedIR[1]).toEqual(1); expect(reconstructedIR[1]).toEqual(1);
expect(Array.from(reconstructedIR[2])).toEqual( expect(Array.from(reconstructedIR[2])).toEqual(Array.from(ir[2]));
Array.from(shading.coords) expect(Array.from(reconstructedIR[3])).toEqual(Array.from(ir[3]));
);
expect(Array.from(reconstructedIR[3])).toEqual(
Array.from(shading.colors)
);
expect(Array.from(reconstructedIR[7])).toEqual([64, 128, 191]); expect(Array.from(reconstructedIR[7])).toEqual([64, 128, 191]);
}); });
@ -134,8 +111,8 @@ describe("pattern", function () {
}); });
const [, , , colors] = shading.getIR(); const [, , , colors] = shading.getIR();
expect(colors.length).toEqual(48); expect(colors.length).toEqual(144);
expect(Array.from(colors)).toEqual(new Array(48).fill(0)); expect(Array.from(colors)).toEqual(new Array(144).fill(0));
}); });
}); });
}); });