Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Apps/UnitTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,20 @@ set(SOURCES
"Source/Tests.JavaScript.cpp"
"Source/Tests.NativeEngine.InstanceData.cpp"
"Source/Tests.NativeEngine.ImageFormats.cpp"
"Source/Tests.NativeEngine.StorageBuffer.cpp"
"Source/Tests.NativeEngine.Compute.cpp"
"Source/Tests.NativeEngine.Mipmaps.cpp"
"Source/Tests.NativeEngine.PrimitiveModeExpansion.cpp"
"Source/Tests.NativeEngine.DepthTextures.cpp"
"Source/Tests.NativeEngine.CubeRenderTargets.cpp"
"Source/Tests.NativeEngine.Msaa.cpp"
"Source/Tests.NativeEngine.Teardown.cpp"
"Source/Tests.NativeEngine.TransformFeedback.cpp"
"Source/Tests.NativeEngine.TextureFormats.cpp"
"Source/Tests.NativeOptimizations.cpp"
"Source/Tests.NativeEngine.VertexArray.cpp"
"Source/Tests.ShaderCache.cpp"
"Source/Tests.ShaderCache.TransformFeedback.cpp"
"Source/Tests.ShaderCompilation.cpp"
"Source/Tests.ShaderCompilation.FragCoord.cpp"
"Source/Tests.UniformPadding.cpp"
Expand Down Expand Up @@ -112,6 +117,7 @@ target_link_libraries(UnitTests
PRIVATE NativeEncoding
PRIVATE ScriptLoader
PRIVATE ShaderCache
PRIVATE ShaderCacheInternal
PRIVATE Window
PRIVATE XMLHttpRequest
PRIVATE gtest_main
Expand Down Expand Up @@ -186,9 +192,13 @@ if(GRAPHICS_API STREQUAL "OpenGL")
# OpenGL the "device" is a context bgfx created, and a bare context carries no display or
# surface, so Helpers::CreateDevice cannot make one current to allocate against.
# See https://github.com/BabylonJS/BabylonNative/issues/1810.
#
# Flipped sampling: the sampler and texture row flips these tests cover only run on
# top-left-origin backends; GLSL samples in GL's native bottom-left orientation.
target_compile_definitions(UnitTests PRIVATE
SKIP_MULTISAMPLE_TESTS
SKIP_DEVICE_LOSS_TESTS)
SKIP_DEVICE_LOSS_TESTS
SKIP_FLIPPED_SAMPLING_TESTS)
endif()

add_test(NAME UnitTests COMMAND UnitTests)
Expand Down
470 changes: 371 additions & 99 deletions Apps/UnitTests/JavaScript/dist/tests.javaScript.all.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Apps/UnitTests/JavaScript/src/tests.javaScript.all.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as Mocha from "mocha";
import { expect } from "chai";
import { registerTriangleStripDepthTests } from "./tests.nativeEngine.triangleStripDepth";
import { registerIblCdfTests } from "./tests.nativeEngine.iblCdf";
import { registerPngTests } from "./tests.nativeEngine.png";
import { registerCanvasImageTests } from "./tests.nativeEngine.canvasImage";
import { Buffer } from "buffer";
Expand Down Expand Up @@ -42,6 +44,9 @@ declare const setExitCode: (code: number) => void;
declare const setImageReloadTestResponse: (bytes: Uint8Array) => void;
declare const skipCanvasGpuTests: boolean;
declare const _native: any;

registerTriangleStripDepthTests(describe, it, skipCanvasGpuTests);
registerIblCdfTests(describe, it, skipCanvasGpuTests);
registerPngTests(describe, it, hasGpuRendering && hasNativeImageLoading);
registerAttributeLessInstancingTests(describe, it, hasAttributeLessInstancing);

Expand Down
48 changes: 43 additions & 5 deletions Apps/UnitTests/JavaScript/src/tests.nativeEngine.canvasImage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Mocha from "mocha";
import { expect } from "chai";
import { Constants, DynamicTexture, NativeEngine, Scene } from "@babylonjs/core";
import { Constants, DynamicTexture, FreeCamera, NativeEngine, Scene, Vector3 } from "@babylonjs/core";

type CanvasContext = ReturnType<DynamicTexture["getContext"]>;

Expand All @@ -16,8 +16,8 @@ function fillImageData(context: CanvasContext, width: number, height: number, pi
context.putImageData(imageData, dx, dy);
}

async function readTexture(texture: DynamicTexture): Promise<Uint8Array> {
texture.update(false);
async function readTexture(texture: DynamicTexture, premulAlpha = false): Promise<Uint8Array> {
texture.update(false, premulAlpha);
const pixels = await texture.readPixels();
if (!(pixels instanceof Uint8Array)) {
throw new Error("Expected RGBA8 GPU readback for the canvas texture");
Expand All @@ -41,6 +41,44 @@ export function registerCanvasImageTests(
this.timeout(10000);
const test = skipCanvasGpuTests ? it.skip : it;

test("copies regenerated Canvas mipmaps in the requested alpha representation", async function () {
const engine = new NativeEngine();
const scene = new Scene(engine);
try {
const texture = new DynamicTexture("Canvas mip chain", { width: 8, height: 8 }, scene, true, Constants.TEXTURE_TRILINEAR_SAMPLINGMODE);
if (!texture.getInternalTexture()?.generateMipMaps) {
this.skip();
}
const context = texture.getContext();
for (const premulAlpha of [false, true]) {
for (const blue of [true, false]) {
context.clearRect(0, 0, 8, 8);
fillImageData(context, 8, 8, (x, y) => (
(x + y) % 2 === 0 ? [255, 0, 0, 102] : blue ? [0, 0, 255, 102] : [0, 255, 0, 102]
));
texture.update(false, premulAlpha);
for (const mip of [1, 2, 3]) {
const pixels = await texture.readPixels(0, mip);
if (!(pixels instanceof Uint8Array)) {
throw new Error("Expected RGBA8 Canvas mip readback");
}
const rgba = pixels;
const size = 8 >> mip;
expect(rgba.length).to.equal(size * size * 4);
const half = premulAlpha ? 51 : 128;
const expected = blue ? [half, 0, half, 102] : [half, half, 0, 102];
for (let index = 0; index < rgba.length; ++index) {
expect(rgba[index], `mip ${mip}, byte ${index}`).to.be.closeTo(expected[index % 4], 1);
}
}
}
}
} finally {
scene.dispose();
engine.dispose();
}
});

test("keeps putImageData pixels alive through the GPU flush", async function () {
const engine = new NativeEngine();
const scene = new Scene(engine);
Expand Down Expand Up @@ -126,8 +164,8 @@ export function registerCanvasImageTests(
const source = engine.createCanvas(18, 8);
fillImageData(source.getContext("2d"), 18, 8, (x) => (
x < 6 ? [255, 0, 0, 255] :
x < 12 ? [0, 255, 0, 255] :
[0, 0, 255, 255]
x < 12 ? [0, 255, 0, 255] :
[0, 0, 255, 255]
));

const texture = new DynamicTexture(
Expand Down
82 changes: 82 additions & 0 deletions Apps/UnitTests/JavaScript/src/tests.nativeEngine.iblCdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as Mocha from "mocha";
import { expect } from "chai";
import {
Constants,
FreeCamera,
MeshBuilder,
NativeEngine,
RawTexture,
RenderTargetTexture,
Scene,
ShaderMaterial,
Vector3,
} from "@babylonjs/core";

export function registerIblCdfTests(
describe: typeof Mocha.describe,
it: typeof Mocha.it,
skipCanvasGpuTests: boolean
): void {
describe("NativeEngine IBL CDF texel bins", function () {
this.timeout(10000);
(skipCanvasGpuTests ? it.skip : it)("preserves boundary ties, adjacent bins, and clamped endpoints", async function () {
const engine = new NativeEngine();
const scene = new Scene(engine);
try {
const camera = new FreeCamera("camera", new Vector3(0, 0, -3), scene);
camera.setTarget(Vector3.Zero());
const data = new Uint8Array(4 * 4 * 4);
for (let y = 0; y < 4; y++) {
for (let x = 0; x < 4; x++) {
data.set([x * 64, y * 64, 0, 255], (y * 4 + x) * 4);
}
}
const texture = RawTexture.CreateRGBATexture(data, 4, 4, scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE);
texture.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE;
texture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE;
const material = new ShaderMaterial("cdfBins", scene, {
vertexSource: `
precision highp float;
attribute vec3 position;
void main() { gl_Position = vec4(position.xy, 0.0, 1.0); }`,
fragmentSource: `
precision highp float;
uniform sampler2D icdf;
void main() {
float index = floor(gl_FragCoord.x);
float coordinate = index / 4.0;
if (index == 5.0) coordinate = -1.0;
if (index == 6.0) coordinate = 1.5;
if (index == 7.0) coordinate = 0.5 - 1.0 / 1024.0;
if (index == 8.0) coordinate = 0.5 + 1.0 / 1024.0;
gl_FragColor = texture2D(icdf, vec2(coordinate));
}`,
}, { attributes: ["position"], uniforms: [], samplers: ["icdf"] });
material.backFaceCulling = false;
material.setTexture("icdf", texture);
const plane = MeshBuilder.CreatePlane("plane", { size: 2 }, scene);
plane.material = material;
const target = new RenderTargetTexture("cdfBins", { width: 9, height: 1 }, scene, {
generateDepthBuffer: false,
samples: 1,
});
target.renderList = [plane];
await material.forceCompilationAsync(plane);
await scene.whenReadyAsync();
target.render();
const pixels = await target.readPixels();
if (!(pixels instanceof Uint8Array)) {
throw new Error("Expected RGBA8 CDF bin readback");
}
const expectedBins = [0, 1, 2, 3, 3, 0, 3, 1, 2];
for (let i = 0; i < expectedBins.length; i++) {
const value = expectedBins[i] * 64;
expect(Array.from(pixels.subarray(i * 4, i * 4 + 4)), `CDF query ${i}`).to.deep.equal([value, value, 0, 255]);
}
} finally {
scene.dispose();
engine.dispose();
}
});
});
}
3 changes: 2 additions & 1 deletion Apps/UnitTests/JavaScript/src/tests.nativeEngine.png.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Mocha from "mocha";
import { expect } from "chai";
import { Constants, NativeEngine, Scene, Texture } from "@babylonjs/core";
import { Constants, NativeEngine, RawTexture, Scene, Texture } from "@babylonjs/core";

const fixtures: { name: string; png: string; pixels: number[]; width?: number; height?: number; tolerance?: number }[] = [
{
Expand Down Expand Up @@ -142,5 +142,6 @@ export function registerPngTests(
}
});
}

});
}
127 changes: 127 additions & 0 deletions Apps/UnitTests/JavaScript/src/tests.nativeEngine.triangleStripDepth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as Mocha from "mocha";
import { expect } from "chai";
import {
Camera,
Color4,
FreeCamera,
Material,
Matrix,
Mesh,
NativeEngine,
RenderTargetTexture,
Scene,
ShaderMaterial,
Vector3,
VertexBuffer,
} from "@babylonjs/core";

async function renderTriangleStripDepthOverlap(instanced: boolean, depthWrite: boolean): Promise<number[]> {
const engine = new NativeEngine();
const scene = new Scene(engine);
try {
const camera = new FreeCamera("camera", new Vector3(0, 0, -3), scene);
camera.setTarget(Vector3.Zero());
camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
camera.orthoTop = 1;
camera.orthoBottom = -1;
camera.orthoLeft = -1;
camera.orthoRight = 1;
scene.activeCamera = camera;

const material = new ShaderMaterial(
"triangleStripDepth",
scene,
{
vertexSource: `
precision highp float;
attribute vec3 position;
uniform mat4 viewProjection;
#include<instancesDeclaration>
varying vec3 vColor;
void main(void) {
#include<instancesVertex>
gl_Position = viewProjection * finalWorld * vec4(position, 1.0);
vColor = finalWorld[3].z < 0.25 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 0.0, 1.0);
}`,
fragmentSource: `
precision highp float;
varying vec3 vColor;
void main(void) {
gl_FragColor = vec4(vColor, 1.0);
}`,
},
{
attributes: ["position"],
uniforms: ["world", "viewProjection"],
}
);
material.fillMode = Material.TriangleStripDrawMode;
material.disableDepthWrite = !depthWrite;

const createStrip = (name: string): Mesh => {
const mesh = new Mesh(name, scene);
mesh.setVerticesData(VertexBuffer.PositionKind, [-0.8, -0.8, 0, 0.8, -0.8, 0, -0.8, 0.8, 0, 0.8, 0.8, 0]);
mesh.isUnIndexed = true;
mesh.material = material;
return mesh;
};

const renderTarget = new RenderTargetTexture("triangleStripDepthTarget", { width: 16, height: 16 }, scene, {
generateDepthBuffer: true,
generateStencilBuffer: false,
samples: 1,
});
renderTarget.clearColor = new Color4(0, 0, 0, 1);
renderTarget.activeCamera = camera;

if (instanced) {
const strip = createStrip("instancedStrip");
const matrices = new Float32Array(32);
Matrix.Identity().copyToArray(matrices, 0);
Matrix.Translation(0, 0, 0.5).copyToArray(matrices, 16);
strip.thinInstanceSetBuffer("matrix", matrices, 16, true);
renderTarget.renderList!.push(strip);
} else {
const nearStrip = createStrip("nearStrip");
const farStrip = createStrip("farStrip");
farStrip.position.z = 0.5;
nearStrip.alphaIndex = 0;
farStrip.alphaIndex = 1;
renderTarget.setRenderingOrder(0, (a, b) => a.getMesh().alphaIndex - b.getMesh().alphaIndex);
renderTarget.renderList!.push(nearStrip, farStrip);
}

await scene.whenReadyAsync();
renderTarget.render();
const pixels = await renderTarget.readPixels();
if (!(pixels instanceof Uint8Array)) {
throw new Error("Expected RGBA8 triangle-strip render target readback");
}
const offset = (8 * 16 + 8) * 4;
return Array.from(pixels.subarray(offset, offset + 4));
} finally {
scene.dispose();
engine.dispose();
}
}

export function registerTriangleStripDepthTests(
describe: typeof Mocha.describe,
it: typeof Mocha.it,
skipCanvasGpuTests: boolean
): void {
describe("NativeEngine triangle-strip depth writes", function () {
this.timeout(10000);
for (const instanced of [false, true]) {
for (const depthWrite of [false, true]) {
(skipCanvasGpuTests ? it.skip : it)(
`${instanced ? "instanced" : "non-instanced"} strips preserve depthWrite=${depthWrite}`,
async function () {
const pixel = await renderTriangleStripDepthOverlap(instanced, depthWrite);
expect(pixel).to.deep.equal(depthWrite ? [255, 0, 0, 255] : [0, 0, 255, 255]);
}
);
}
}
});
}
Loading
Loading