@quake2ts/test-utils 0.0.764 → 0.0.766
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +54 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -7
- package/dist/index.d.ts +25 -7
- package/dist/index.js +52 -56
- package/dist/index.js.map +1 -1
- package/package.json +14 -9
- package/src/engine/helpers/webgl-rendering.ts +47 -0
- package/src/index.ts +5 -2
- package/src/setup/headless-webgl.ts +54 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quake2ts/test-utils",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.766",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"types": "./src/setup/webgpu.ts",
|
|
21
21
|
"import": "./src/setup/webgpu.ts",
|
|
22
22
|
"require": "./src/setup/webgpu.ts"
|
|
23
|
+
},
|
|
24
|
+
"./src/setup/headless-webgl": {
|
|
25
|
+
"types": "./src/setup/headless-webgl.ts",
|
|
26
|
+
"import": "./src/setup/headless-webgl.ts",
|
|
27
|
+
"require": "./src/setup/headless-webgl.ts"
|
|
23
28
|
}
|
|
24
29
|
},
|
|
25
30
|
"files": [
|
|
@@ -39,10 +44,10 @@
|
|
|
39
44
|
"pngjs": "^7.0.0",
|
|
40
45
|
"vitest": "^1.6.0",
|
|
41
46
|
"webgpu": "^0.3.8",
|
|
42
|
-
"@quake2ts/engine": "^0.0.
|
|
43
|
-
"@quake2ts/
|
|
44
|
-
"@quake2ts/
|
|
45
|
-
"@quake2ts/shared": "0.0.
|
|
47
|
+
"@quake2ts/engine": "^0.0.766",
|
|
48
|
+
"@quake2ts/game": "0.0.766",
|
|
49
|
+
"@quake2ts/server": "0.0.766",
|
|
50
|
+
"@quake2ts/shared": "0.0.766"
|
|
46
51
|
},
|
|
47
52
|
"peerDependenciesMeta": {
|
|
48
53
|
"@quake2ts/engine": {
|
|
@@ -93,10 +98,10 @@
|
|
|
93
98
|
"typescript": "^5.4.5",
|
|
94
99
|
"vitest": "^1.6.0",
|
|
95
100
|
"webgpu": "^0.3.8",
|
|
96
|
-
"@quake2ts/engine": "^0.0.
|
|
97
|
-
"@quake2ts/game": "0.0.
|
|
98
|
-
"@quake2ts/server": "0.0.
|
|
99
|
-
"@quake2ts/shared": "0.0.
|
|
101
|
+
"@quake2ts/engine": "^0.0.766",
|
|
102
|
+
"@quake2ts/game": "0.0.766",
|
|
103
|
+
"@quake2ts/server": "0.0.766",
|
|
104
|
+
"@quake2ts/shared": "0.0.766"
|
|
100
105
|
},
|
|
101
106
|
"optionalDependencies": {
|
|
102
107
|
"gl": "^8.1.6"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHeadlessWebGL, captureWebGLFramebuffer, HeadlessWebGLContext } from '../../setup/headless-webgl.js';
|
|
2
|
+
// Removed invalid import
|
|
3
|
+
|
|
4
|
+
export interface WebGLRenderTestSetup {
|
|
5
|
+
gl: WebGL2RenderingContext;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
cleanup: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a setup for testing WebGL rendering.
|
|
13
|
+
* Initializes a headless WebGL context.
|
|
14
|
+
*/
|
|
15
|
+
export async function createWebGLRenderTestSetup(
|
|
16
|
+
width: number = 256,
|
|
17
|
+
height: number = 256
|
|
18
|
+
): Promise<WebGLRenderTestSetup> {
|
|
19
|
+
const context = createHeadlessWebGL({ width, height });
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
gl: context.gl,
|
|
23
|
+
width: context.width,
|
|
24
|
+
height: context.height,
|
|
25
|
+
cleanup: context.cleanup
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Helper to render and capture the output as pixel data.
|
|
31
|
+
* Executes the render function, ensures completion, and captures the framebuffer.
|
|
32
|
+
*/
|
|
33
|
+
export async function renderAndCaptureWebGL(
|
|
34
|
+
setup: WebGLRenderTestSetup,
|
|
35
|
+
renderFn: (gl: WebGL2RenderingContext) => void
|
|
36
|
+
): Promise<Uint8ClampedArray> {
|
|
37
|
+
const { gl, width, height } = setup;
|
|
38
|
+
|
|
39
|
+
// Execute the user's render function
|
|
40
|
+
renderFn(gl);
|
|
41
|
+
|
|
42
|
+
// Ensure all commands are finished before reading pixels
|
|
43
|
+
gl.finish();
|
|
44
|
+
|
|
45
|
+
// Capture the framebuffer content
|
|
46
|
+
return captureWebGLFramebuffer(gl, width, height);
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,8 @@ export * from './server/helpers/bandwidth.js';
|
|
|
29
29
|
export * from './setup/browser.js';
|
|
30
30
|
export * from './setup/canvas.js';
|
|
31
31
|
export * from './setup/webgpu.js';
|
|
32
|
-
export
|
|
32
|
+
// Safe to export now as gl is lazy loaded
|
|
33
|
+
export * from './setup/headless-webgl.js';
|
|
33
34
|
export * from './engine/mocks/webgpu.js';
|
|
34
35
|
export * from './setup/timing.js';
|
|
35
36
|
export * from './setup/node.js';
|
|
@@ -44,6 +45,7 @@ export * from './engine/rendering.js';
|
|
|
44
45
|
export * from './setup/storage.js';
|
|
45
46
|
export * from './setup/audio.js';
|
|
46
47
|
export * from './engine/helpers/webgpu-rendering.js';
|
|
48
|
+
export * from './engine/helpers/webgl-rendering.js';
|
|
47
49
|
export * from './engine/helpers/pipeline-test-template.js';
|
|
48
50
|
|
|
49
51
|
// Client Mocks
|
|
@@ -72,8 +74,9 @@ export type { StorageScenario } from './setup/storage.js';
|
|
|
72
74
|
export type { NetworkSimulator, NetworkCondition } from './e2e/network.js';
|
|
73
75
|
export type { VisualScenario, VisualDiff } from './e2e/visual.js';
|
|
74
76
|
export type { HeadlessWebGPUSetup, WebGPUContextState } from './setup/webgpu.js';
|
|
75
|
-
export type { HeadlessWebGLContext, HeadlessWebGLOptions } from './setup/headless-webgl.js';
|
|
77
|
+
export type { HeadlessWebGLContext, HeadlessWebGLOptions } from './setup/headless-webgl.js';
|
|
76
78
|
export type { RenderTestSetup, ComputeTestSetup } from './engine/helpers/webgpu-rendering.js';
|
|
79
|
+
export type { WebGLRenderTestSetup } from './engine/helpers/webgl-rendering.js';
|
|
77
80
|
export type { GeometryBuffers } from './engine/helpers/pipeline-test-template.js';
|
|
78
81
|
|
|
79
82
|
// Shared Types
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Remove top-level import to avoid runtime crash when gl is missing
|
|
2
|
+
// import createGL from 'gl';
|
|
3
|
+
import type { WebGLContextState } from '@quake2ts/engine';
|
|
4
|
+
|
|
1
5
|
export interface HeadlessWebGLOptions {
|
|
2
6
|
width?: number;
|
|
3
7
|
height?: number;
|
|
@@ -14,99 +18,98 @@ export interface HeadlessWebGLContext {
|
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Creates a headless WebGL2 context using the 'gl' package.
|
|
17
|
-
*
|
|
21
|
+
* This is used for running WebGL tests in a Node.js environment without a browser.
|
|
18
22
|
*/
|
|
19
|
-
export
|
|
23
|
+
export function createHeadlessWebGL(
|
|
20
24
|
options: HeadlessWebGLOptions = {}
|
|
21
|
-
):
|
|
25
|
+
): HeadlessWebGLContext {
|
|
22
26
|
const width = options.width ?? 256;
|
|
23
27
|
const height = options.height ?? 256;
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
let createGL;
|
|
30
|
+
try {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
createGL = require('gl');
|
|
33
|
+
} catch (e) {
|
|
34
|
+
throw new Error('gl package not found or failed to load. Install it to run WebGL tests.');
|
|
35
|
+
}
|
|
28
36
|
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
antialias: options.antialias ?? false, // Default to false for
|
|
32
|
-
preserveDrawingBuffer: options.preserveDrawingBuffer ?? true, //
|
|
37
|
+
// Create WebGL context using 'gl' package
|
|
38
|
+
const glContext = createGL(width, height, {
|
|
39
|
+
antialias: options.antialias ?? false, // Default to false for deterministic testing
|
|
40
|
+
preserveDrawingBuffer: options.preserveDrawingBuffer ?? true, // Needed for readback
|
|
33
41
|
stencil: true,
|
|
34
|
-
alpha: true,
|
|
35
42
|
depth: true,
|
|
43
|
+
alpha: true
|
|
36
44
|
});
|
|
37
45
|
|
|
38
|
-
if (!
|
|
46
|
+
if (!glContext) {
|
|
39
47
|
throw new Error('Failed to create headless WebGL context');
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
// Cast to WebGL2RenderingContext
|
|
43
|
-
|
|
50
|
+
// Cast to WebGL2RenderingContext as 'gl' returns a compatible interface
|
|
51
|
+
// but TypeScript types might not align perfectly without casting
|
|
52
|
+
const gl = glContext as unknown as WebGL2RenderingContext;
|
|
53
|
+
|
|
54
|
+
// Verify context creation
|
|
55
|
+
const version = gl.getParameter(gl.VERSION);
|
|
56
|
+
// console.log(`Created headless WebGL context: ${version}`);
|
|
57
|
+
|
|
58
|
+
// Create cleanup function
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
const ext = gl.getExtension('STACKGL_destroy_context');
|
|
61
|
+
if (ext) {
|
|
62
|
+
ext.destroy();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
44
65
|
|
|
45
66
|
return {
|
|
46
|
-
gl
|
|
67
|
+
gl,
|
|
47
68
|
width,
|
|
48
69
|
height,
|
|
49
|
-
cleanup
|
|
50
|
-
// gl package extension to destroy context
|
|
51
|
-
const ext = glContext.getExtension('STACKGL_destroy_context');
|
|
52
|
-
if (ext) {
|
|
53
|
-
ext.destroy();
|
|
54
|
-
}
|
|
55
|
-
},
|
|
70
|
+
cleanup
|
|
56
71
|
};
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
/**
|
|
60
|
-
* Captures the current framebuffer content as a
|
|
61
|
-
*
|
|
75
|
+
* Captures the current framebuffer content as a pixel array.
|
|
76
|
+
* Performs a vertical flip to match standard image coordinates (top-left origin).
|
|
62
77
|
*/
|
|
63
78
|
export function captureWebGLFramebuffer(
|
|
64
|
-
|
|
79
|
+
gl: WebGL2RenderingContext,
|
|
65
80
|
width: number,
|
|
66
81
|
height: number
|
|
67
82
|
): Uint8ClampedArray {
|
|
68
83
|
const pixels = new Uint8ClampedArray(width * height * 4);
|
|
69
84
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
0,
|
|
73
|
-
0,
|
|
74
|
-
width,
|
|
75
|
-
height,
|
|
76
|
-
glContext.RGBA,
|
|
77
|
-
glContext.UNSIGNED_BYTE,
|
|
78
|
-
pixels
|
|
79
|
-
);
|
|
85
|
+
// Read pixels from framebuffer (bottom-left origin)
|
|
86
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
80
87
|
|
|
88
|
+
// Flip vertically to match image coordinates (top-left origin)
|
|
81
89
|
return flipPixelsVertically(pixels, width, height);
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
/**
|
|
85
|
-
* Flips pixel
|
|
93
|
+
* Flips a pixel array vertically in-place or returns a new array.
|
|
94
|
+
* WebGL reads pixels bottom-up, but images are typically stored top-down.
|
|
86
95
|
*/
|
|
87
96
|
export function flipPixelsVertically(
|
|
88
97
|
pixels: Uint8ClampedArray,
|
|
89
98
|
width: number,
|
|
90
99
|
height: number
|
|
91
100
|
): Uint8ClampedArray {
|
|
101
|
+
const flipped = new Uint8ClampedArray(pixels.length);
|
|
92
102
|
const rowSize = width * 4;
|
|
93
|
-
const halfHeight = Math.floor(height / 2);
|
|
94
|
-
const tempRow = new Uint8Array(rowSize);
|
|
95
|
-
|
|
96
|
-
// Swap rows
|
|
97
|
-
for (let y = 0; y < halfHeight; y++) {
|
|
98
|
-
const topOffset = y * rowSize;
|
|
99
|
-
const bottomOffset = (height - 1 - y) * rowSize;
|
|
100
|
-
|
|
101
|
-
// Copy top to temp
|
|
102
|
-
tempRow.set(pixels.subarray(topOffset, topOffset + rowSize));
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
for (let y = 0; y < height; y++) {
|
|
105
|
+
const srcRowStart = y * rowSize;
|
|
106
|
+
const destRowStart = (height - 1 - y) * rowSize;
|
|
106
107
|
|
|
107
|
-
// Copy
|
|
108
|
-
|
|
108
|
+
// Copy row
|
|
109
|
+
for (let i = 0; i < rowSize; i++) {
|
|
110
|
+
flipped[destRowStart + i] = pixels[srcRowStart + i];
|
|
111
|
+
}
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
return
|
|
114
|
+
return flipped;
|
|
112
115
|
}
|