test(3d): screenshot-baseline TDD suite for the 3D viewer OpenGL->WebGL port — 47 native goldens + red-state WebGL harness
tests/3d-regression mirrors the gal-regression pattern at renderer scale: shared C++ scenarios call real KiCad 3D-viewer code (opengl_utils, display lists + DrawCulled stencil subtraction, MODEL_3D VBOs, private generators via a rob-template accessor, and full reload()+Redraw() composites over a synthetic BOARD_ADAPTER). A native macOS harness renders them on real OpenGL into 47 committed goldens (bit-deterministic, FBO capture); the wasm harness compiles the same TUs against wasm/stubs/gl_ffp_stub.c no-ops so every scenario renders blank — the TDD red state (parity meter: 47/47 changed). Comparisons use the CI pixelmatch engine via the new generic compare-dirs.ts (floors.json levels; manifest.json cmp-guards registry drift). Documents an upstream bug: appendPostMachiningGeometry's countersink path adds middle quads without normals, silently erasing the walls of any display list it is batched into (3d-post-machining.png keeps the lists separate to record it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1a24c2894e
commit
ce1473c9ab
85 changed files with 6797 additions and 1 deletions
120
tests/3d-regression/wasm/3d_webgl_test.cpp
Normal file
120
tests/3d-regression/wasm/3d_webgl_test.cpp
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* 3D-renderer WebGL test harness (WASM) — runs the shared scenarios
|
||||
* (tests/3d-regression/scenarios/) in the browser. Today the legacy GL
|
||||
* surface is wasm/stubs/gl_ffp_stub.c no-ops, so every scenario renders a
|
||||
* black canvas: the TDD red state the WebGL port turns green.
|
||||
*
|
||||
* Unlike the GAL harness this needs no wx window: the renderer draws into
|
||||
* whatever context is current, so a direct emscripten WebGL2 context on
|
||||
* #canvas is enough — created with the attributes the suite requires
|
||||
* (stencil for DrawCulled, no MSAA to match the native single-sample FBO,
|
||||
* preserveDrawingBuffer for Playwright canvas screenshots).
|
||||
*/
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
|
||||
#include "scene3d_test_ctx.h"
|
||||
#include "scene3d_test_scenarios.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
static const int CAPTURE_WIDTH = 800; // must match manifest.json + native FBO
|
||||
static const int CAPTURE_HEIGHT = 600;
|
||||
|
||||
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_context = 0;
|
||||
static SCENE3D_CTX* g_ctx = nullptr;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int getTotalScenarios()
|
||||
{
|
||||
return Scene3DTest::GetScenarioCount();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
const char* getScenarioName( int aIndex )
|
||||
{
|
||||
return Scene3DTest::GetScenarioName( aIndex );
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int getCanvasWidth()
|
||||
{
|
||||
return CAPTURE_WIDTH;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int getCanvasHeight()
|
||||
{
|
||||
return CAPTURE_HEIGHT;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int runScenario( int aIndex )
|
||||
{
|
||||
if( aIndex < 0 || aIndex >= Scene3DTest::GetScenarioCount() )
|
||||
return -1;
|
||||
|
||||
if( emscripten_webgl_make_context_current( g_context ) != EMSCRIPTEN_RESULT_SUCCESS )
|
||||
return -2;
|
||||
|
||||
if( !g_ctx )
|
||||
{
|
||||
g_ctx = new SCENE3D_CTX( CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
||||
g_ctx->InitOnce();
|
||||
}
|
||||
|
||||
std::printf( "[3d-webgl] scenario %d: %s\n", aIndex, Scene3DTest::GetScenarioName( aIndex ) );
|
||||
|
||||
// Start from a cleared frame; scenarios call BeginFrame themselves.
|
||||
glViewport( 0, 0, CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
||||
glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );
|
||||
glClearDepth( 1.0f );
|
||||
glClearStencil( 0 );
|
||||
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
|
||||
|
||||
Scene3DTest::RenderScenario( *g_ctx, aIndex );
|
||||
|
||||
glFinish();
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
EmscriptenWebGLContextAttributes attrs;
|
||||
emscripten_webgl_init_context_attributes( &attrs );
|
||||
|
||||
attrs.majorVersion = 2;
|
||||
attrs.minorVersion = 0;
|
||||
attrs.alpha = false;
|
||||
attrs.depth = true;
|
||||
attrs.stencil = true; // DrawCulled hole subtraction
|
||||
attrs.antialias = false; // native goldens are single-sample
|
||||
attrs.preserveDrawingBuffer = true; // Playwright canvas.screenshot()
|
||||
|
||||
emscripten_set_canvas_element_size( "#canvas", CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
||||
|
||||
g_context = emscripten_webgl_create_context( "#canvas", &attrs );
|
||||
|
||||
if( g_context <= 0 )
|
||||
{
|
||||
std::fprintf( stderr, "[3d-webgl] failed to create WebGL2 context (%ld)\n",
|
||||
(long) g_context );
|
||||
return 1;
|
||||
}
|
||||
|
||||
emscripten_webgl_make_context_current( g_context );
|
||||
|
||||
std::printf( "[3d-webgl] ready: %d scenarios, %dx%d\n", Scene3DTest::GetScenarioCount(),
|
||||
CAPTURE_WIDTH, CAPTURE_HEIGHT );
|
||||
|
||||
EM_ASM( { if( window._threeDTestReady ) window._threeDTestReady(); } );
|
||||
|
||||
return 0;
|
||||
}
|
||||
35
tests/3d-regression/wasm/3d_webgl_test.html
Normal file
35
tests/3d-regression/wasm/3d_webgl_test.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>3D Renderer WebGL Test</title>
|
||||
<style>
|
||||
body { margin: 0; background: #202020; }
|
||||
#canvas { display: block; width: 800px; height: 600px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" width="800" height="600"></canvas>
|
||||
<script>
|
||||
let ready = false;
|
||||
let wasmModule = null;
|
||||
|
||||
window._threeDTestReady = () => { ready = true; };
|
||||
|
||||
window.threeDTest = {
|
||||
isReady: () => ready,
|
||||
runScenario: (i) => wasmModule.ccall('runScenario', 'number', ['number'], [i]),
|
||||
getTotalScenarios: () => wasmModule.ccall('getTotalScenarios', 'number', [], []),
|
||||
getScenarioName: (i) => wasmModule.ccall('getScenarioName', 'string', ['number'], [i]),
|
||||
getCanvasWidth: () => wasmModule.ccall('getCanvasWidth', 'number', [], []),
|
||||
getCanvasHeight: () => wasmModule.ccall('getCanvasHeight', 'number', [], []),
|
||||
};
|
||||
</script>
|
||||
<script src="3d_webgl_test.js"></script>
|
||||
<script>
|
||||
create3DTest({ canvas: document.getElementById('canvas') })
|
||||
.then((mod) => { wasmModule = mod; })
|
||||
.catch((err) => { console.error('[3d-webgl] module init failed:', err); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
194
tests/3d-regression/wasm/Makefile
Normal file
194
tests/3d-regression/wasm/Makefile
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Makefile for the 3D-renderer WebGL test harness (WASM) — TDD RED STATE.
|
||||
#
|
||||
# Compiles the SAME shared scenarios + real KiCad 3D-viewer TUs as the native
|
||||
# golden generator (tests/3d-regression/native/CMakeLists.txt), but links the
|
||||
# fixed-function GL surface against wasm/stubs/gl_ffp_stub.c no-ops — exactly
|
||||
# how the production kicad build satisfies RENDER_3D_OPENGL's link today.
|
||||
# Every scenario therefore renders BLANK until the WebGL port replaces those
|
||||
# stubs; `npm run 3d:check:parity` is the port-progress meter.
|
||||
#
|
||||
# Usage:
|
||||
# make # Build
|
||||
# make clean # Clean build artifacts
|
||||
# make DEBUG=1 # Debug build with source maps
|
||||
|
||||
CXX = em++
|
||||
CC = emcc
|
||||
|
||||
PROJECT_ROOT = ../../..
|
||||
KICAD_ROOT = $(PROJECT_ROOT)/kicad
|
||||
WX_BUILD = $(PROJECT_ROOT)/build-wasm/wxwidgets
|
||||
|
||||
OUTPUT_DIR = ../../apps/3d-webgl
|
||||
|
||||
# wxWidgets WASM (the KiCad TUs reference wxString/wxLogTrace/wxASSERT).
|
||||
WXCONFIG = $(WX_BUILD)/wx-config
|
||||
WX_CXXFLAGS := $(shell $(WXCONFIG) --cxxflags)
|
||||
WX_LDFLAGS := $(shell $(WXCONFIG) --libs base,core,gl)
|
||||
|
||||
# Sysroot (boost, glm, ...)
|
||||
SYSROOT = $(PROJECT_ROOT)/build-wasm/sysroot
|
||||
|
||||
# Mirrors the include set of tests/3d-regression/native/CMakeLists.txt.
|
||||
KICAD_INCLUDES = -I../native \
|
||||
-I../scenarios \
|
||||
-I$(KICAD_ROOT)/include \
|
||||
-I$(KICAD_ROOT)/3d-viewer \
|
||||
-I$(KICAD_ROOT)/3d-viewer/3d_rendering \
|
||||
-I$(KICAD_ROOT)/3d-viewer/3d_viewer \
|
||||
-I$(KICAD_ROOT)/pcbnew \
|
||||
-I$(KICAD_ROOT)/common \
|
||||
-I$(KICAD_ROOT)/libs/kimath/include \
|
||||
-I$(KICAD_ROOT)/libs/core/include \
|
||||
-I$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/include \
|
||||
-I$(KICAD_ROOT)/thirdparty/dynamic_bitset \
|
||||
-I$(KICAD_ROOT)/thirdparty/rtree \
|
||||
-I$(KICAD_ROOT)/thirdparty/magic_enum/magic_enum \
|
||||
-I$(KICAD_ROOT)/thirdparty/expected/include \
|
||||
-I$(KICAD_ROOT)/thirdparty/nlohmann_json \
|
||||
-I$(KICAD_ROOT)/thirdparty \
|
||||
-I$(PROJECT_ROOT)/wasm/stubs \
|
||||
-I$(SYSROOT)/include
|
||||
|
||||
ifdef DEBUG
|
||||
OPT_FLAGS = -g -O0
|
||||
DEBUG_LDFLAGS = -g -gsource-map
|
||||
else
|
||||
OPT_FLAGS = -O1
|
||||
DEBUG_LDFLAGS =
|
||||
endif
|
||||
|
||||
# Match the wx/KiCad WASM exception model (scripts/common/env.sh DEPS_EH_FLAGS).
|
||||
DEPS_EH_FLAGS ?= -fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=1
|
||||
|
||||
# USINGZ: KiCad builds clipper2 with it (PUBLIC compile definition).
|
||||
CXXFLAGS = $(OPT_FLAGS) $(DEPS_EH_FLAGS) -DUSINGZ $(WX_CXXFLAGS) $(KICAD_INCLUDES) -std=c++20 -MMD -MP
|
||||
CFLAGS = $(OPT_FLAGS) $(DEPS_EH_FLAGS) -I$(PROJECT_ROOT)/wasm/stubs
|
||||
|
||||
BASE_LDFLAGS = $(DEPS_EH_FLAGS) \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-sEXPORTED_FUNCTIONS=['_main','_runScenario','_getTotalScenarios','_getScenarioName','_getCanvasWidth','_getCanvasHeight'] \
|
||||
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap'] \
|
||||
-sMODULARIZE=1 \
|
||||
-sEXPORT_NAME='create3DTest' \
|
||||
-sENVIRONMENT=web,worker
|
||||
|
||||
# Pure WebGL 2.0 context; the FFP calls are stubbed C no-ops (no
|
||||
# LEGACY_GL_EMULATION), same as the production kicad_editor build.
|
||||
EM_GL_FLAGS = -sMAX_WEBGL_VERSION=2
|
||||
|
||||
LDFLAGS = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) $(EM_GL_FLAGS) $(WX_LDFLAGS)
|
||||
|
||||
MAIN_SRCS = 3d_webgl_test.cpp
|
||||
|
||||
SCENARIO_SRCS = ../scenarios/scene3d_test_scenarios.cpp \
|
||||
../scenarios/scene3d_test_ctx.cpp \
|
||||
../scenarios/test_board_data.cpp \
|
||||
$(wildcard ../scenarios/scenario_*.cpp)
|
||||
|
||||
# Test-impl TUs shared with the native harness (BOARD_ADAPTER seam, settings
|
||||
# stub, link stubs, private-member accessor).
|
||||
TESTIMPL_SRCS = ../native/board_adapter_test_impl.cpp \
|
||||
../native/settings_3d_stub.cpp \
|
||||
../native/kicad_stubs_3d.cpp \
|
||||
../native/render3d_test_accessor.cpp
|
||||
|
||||
# Same real-KiCad TU list as the native CMakeLists (minus glad — Emscripten
|
||||
# provides the modern GL surface, gl_ffp_stub.c the legacy one).
|
||||
KICAD_3D_SRCS = \
|
||||
$(KICAD_ROOT)/3d-viewer/common_ogl/ogl_utils.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/image.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/buffers_debug.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/color_rgba.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/track_ball.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/trackball.cpp \
|
||||
$(KICAD_ROOT)/common/gal/3d/camera.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/layer_triangles.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/opengl_utils.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/3d_model.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/3d_spheres_gizmo.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/ray.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/accelerators/container_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/object_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/bbox_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/round_segment_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/filled_circle_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes3D/bbox_3d.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/trigo.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/eda_angle.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/seg.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/math/util.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/create_scene.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/render_3d_base.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/ring_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/triangle_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/4pt_polygon_2d.cpp \
|
||||
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/polygon_2d.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_poly_set.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_line_chain.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_arc.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/vertex_set.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/geometry_utils.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/convert_basic_shapes_to_polygon.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/md5_hash.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/bezier_curves.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/circle.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/arc_chord_params.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/corner_operations.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_collisions.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_compound.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_rect.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_nearest_points.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/half_line.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_utils.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/roundrect.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/line.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_segment.cpp \
|
||||
$(KICAD_ROOT)/libs/kimath/src/math/vector2.cpp \
|
||||
$(KICAD_ROOT)/common/layer_id.cpp \
|
||||
$(KICAD_ROOT)/common/gal/color4d.cpp \
|
||||
$(KICAD_ROOT)/common/kicad_gl/gl_context_mgr.cpp \
|
||||
$(KICAD_ROOT)/common/lset.cpp \
|
||||
$(KICAD_ROOT)/libs/core/utf8.cpp \
|
||||
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.engine.cpp \
|
||||
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp \
|
||||
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp
|
||||
|
||||
# THE RED STATE: legacy fixed-function GL as no-ops.
|
||||
FFP_STUB_SRCS = $(PROJECT_ROOT)/wasm/stubs/gl_ffp_stub.c
|
||||
|
||||
SRCS = $(MAIN_SRCS) $(SCENARIO_SRCS) $(TESTIMPL_SRCS) $(KICAD_3D_SRCS)
|
||||
OBJS = $(SRCS:.cpp=.o) $(FFP_STUB_SRCS:.c=.o)
|
||||
DEPS = $(SRCS:.cpp=.d)
|
||||
|
||||
TARGET = $(OUTPUT_DIR)/3d_webgl_test.js
|
||||
|
||||
all: $(OUTPUT_DIR) $(TARGET)
|
||||
|
||||
$(OUTPUT_DIR):
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
|
||||
%.o: %.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
||||
%.o: %.c
|
||||
$(CC) -c $(CFLAGS) $< -o $@
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
@echo "Linking $(words $(OBJS)) objects..."
|
||||
$(CXX) $(OBJS) $(LDFLAGS) -o $@
|
||||
cp 3d_webgl_test.html $(OUTPUT_DIR)/
|
||||
|
||||
clean:
|
||||
rm -f $(OBJS) $(DEPS)
|
||||
rm -f $(OUTPUT_DIR)/3d_webgl_test.js
|
||||
rm -f $(OUTPUT_DIR)/3d_webgl_test.wasm
|
||||
rm -f $(OUTPUT_DIR)/3d_webgl_test.html
|
||||
rm -f ../scenarios/*.o ../scenarios/*.d ../native/*.o ../native/*.d
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
-include $(DEPS)
|
||||
Loading…
Reference in a new issue