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:
Istvan Matejcsok 2026-07-03 09:26:43 +02:00
commit ce1473c9ab
85 changed files with 6797 additions and 1 deletions

View file

@ -0,0 +1,168 @@
# Native golden-baseline generator for the 3D-renderer regression suite.
# Compiles REAL KiCad 3D-viewer sources against desktop OpenGL (macOS 2.1
# compatibility context via the vendored glad loader — no GLEW).
# Modeled on tests/gal-regression/native/CMakeLists.txt.
cmake_minimum_required(VERSION 3.16)
project(scene3d_native_test)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(APPLE)
add_compile_definitions(GL_SILENCE_DEPRECATION)
endif()
# KiCad builds clipper2 with USINGZ (PUBLIC), so every TU touching clipper
# types must agree.
add_compile_definitions(USINGZ)
# System wxWidgets (homebrew)
set(wxWidgets_CONFIG_EXECUTABLE "/opt/homebrew/bin/wx-config")
find_package(wxWidgets REQUIRED COMPONENTS core base gl)
include(${wxWidgets_USE_FILE})
# OpenGL.framework provides GL and GLU on macOS
find_package(OpenGL REQUIRED)
# KiCad source root
set(KICAD_ROOT ${CMAKE_SOURCE_DIR}/../../../kicad)
# Vendored glad loader (the fork's kicad_gl/kiglad.h routes to it natively)
set(GLAD_SOURCES ${KICAD_ROOT}/thirdparty/glad/src/gl.c)
# Real KiCad 3D-viewer TUs — Stage 1 (standalone, no RENDER_3D_OPENGL members).
# Stage 2 adds render_3d_opengl.cpp / create_scene.cpp + the board_adapter /
# settings test impls (see the suite README).
set(KICAD_3D_SOURCES
${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
# display lists / plates / stencil subtraction
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/layer_triangles.cpp
# immediate-mode helpers (arrows, segments, bbox, half-cylinder)
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/opengl_utils.cpp
# 3D model VBO path + navigator gizmo
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/3d_model.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/3d_spheres_gizmo.cpp
# shapes2D objects the scenarios construct (ROUND_SEGMENT_2D et al.)
${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
# kimath bits the above reference (RotatePoint in DrawHalfOpenCylinder)
${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
# ---- Stage 2: RENDER_3D_OPENGL itself ----
${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
# remaining shapes2D types the generators take as inputs
${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
# kimath polygon set + triangulation (createBoard/generateLayerList)
${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
# small common/core TUs referenced by the render TUs
${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
# vendored clipper2 (SHAPE_POLY_SET booleans)
${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
)
set(SCENARIO_SOURCES
${CMAKE_SOURCE_DIR}/../scenarios/scene3d_test_scenarios.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scene3d_test_ctx.cpp
${CMAKE_SOURCE_DIR}/../scenarios/test_board_data.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier1_utils.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier1_tdl.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier1_model.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier2_generators.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier3_composite.cpp
)
add_executable(scene3d_native_test
scene3d_native_test.cpp
fbo_capture.cpp
kicad_stubs_3d.cpp
board_adapter_test_impl.cpp
settings_3d_stub.cpp
render3d_test_accessor.cpp
${SCENARIO_SOURCES}
${KICAD_3D_SOURCES}
${GLAD_SOURCES}
)
target_include_directories(scene3d_native_test PRIVATE
${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/../scenarios
# KiCad includes
${KICAD_ROOT}/include # kicad_gl/, gal/3d/camera.h, plugins/3dapi/
${KICAD_ROOT}/3d-viewer # "3d_rendering/...", common_ogl/, 3d_math.h
${KICAD_ROOT}/3d-viewer/3d_rendering # trackball.cpp does #include <trackball.h>
${KICAD_ROOT}/3d-viewer/3d_viewer # create_scene.cpp: <eda_3d_viewer_frame.h>
${KICAD_ROOT}/pcbnew # pad.h et al. (Stage 2 headers)
${KICAD_ROOT}/common
# KiCad libs
${KICAD_ROOT}/libs/kimath/include
${KICAD_ROOT}/libs/core/include
# KiCad thirdparty
${KICAD_ROOT}/thirdparty/glad/include
${KICAD_ROOT}/thirdparty/clipper2/Clipper2Lib/include
${KICAD_ROOT}/thirdparty/dynamic_bitset
${KICAD_ROOT}/thirdparty/rtree # geometry/rtree.h (shape_poly_set.cpp)
${KICAD_ROOT}/thirdparty/magic_enum/magic_enum # magic_enum.hpp (layer_id.cpp)
${KICAD_ROOT}/thirdparty/expected/include # tl/expected.hpp (library_table.h)
${KICAD_ROOT}/thirdparty
# Homebrew (glm)
/opt/homebrew/include
)
target_link_libraries(scene3d_native_test
${wxWidgets_LIBRARIES}
OpenGL::GL
)
message(STATUS "KiCad root: ${KICAD_ROOT}")
message(STATUS "Building scene3d_native_test with actual KiCad 3D-viewer sources")

View file

@ -0,0 +1,521 @@
/**
* Test-harness definitions of BOARD_ADAPTER's out-of-line members.
*
* The real board_adapter.cpp / create_layer_items.cpp drag the whole pcbnew
* board model + settings machinery in none of which exists in this harness.
* Instead WE define the declared members: an out-of-line definition of a
* declared member function has full private access, so this TU is both the
* stub layer and the synthetic-board-data injection seam (InitSettings).
*
* Bodies marked "verbatim" are copied from board_adapter.cpp and must behave
* identically; bodies marked "test" are simplified board-less variants (any
* behavioral drift shows up as a baseline change and is reviewed there).
*/
#include "kicad_stubs_3d.h"
#include "3d_canvas/board_adapter.h"
#include "3d_rendering/raytracing/shapes2D/filled_circle_2d.h"
#include "3d_rendering/raytracing/shapes2D/round_segment_2d.h"
#include "3d_viewer/eda_3d_viewer_settings.h"
#include <board_design_settings.h>
#include <convert_basic_shapes_to_polygon.h>
#include <geometry/geometry_utils.h> // GetArcToSegmentCount
// Same values as board_adapter.cpp:51-57 (defined there as TU-local macros).
#define DEFAULT_BOARD_THICKNESS pcbIUScale.mmToIU( 1.6 )
#define DEFAULT_COPPER_THICKNESS pcbIUScale.mmToIU( 0.035 )
#define DEFAULT_TECH_LAYER_THICKNESS pcbIUScale.mmToIU( 0.025 )
#define SOLDERPASTE_LAYER_THICKNESS pcbIUScale.mmToIU( 0.04 )
#include "../scenarios/test_board_data.h"
// Statics (board_adapter.cpp:60-89, verbatim).
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_SilkColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_MaskColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_PasteColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_FinishColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_BoardColors;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultBackgroundTop;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultBackgroundBot;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSilkscreen;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSolderMask;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSolderPaste;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSurfaceFinish;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultBoardBody;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultComments;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultECOs;
const wxChar* BOARD_ADAPTER::m_logTrace = wxT( "KI_TRACE_EDA_CINFO3D_VISU" );
// The raytracer bevel global lives in board_adapter.cpp too.
float g_BevelThickness3DU = 0.0f;
// test: same default block as the real ctor (board_adapter.cpp:92-157) minus
// ReloadColorSettings() (our version below is settings-free) and the custom
// stackup color tables (not needed — GetLayerColors below is default-based).
BOARD_ADAPTER::BOARD_ADAPTER() :
m_Cfg( nullptr ),
m_IsBoardView( true ),
m_MousewheelPanning( true ),
m_IsPreviewer( false ),
m_board( nullptr ),
m_3dModelManager( nullptr ),
m_layerZcoordTop(),
m_layerZcoordBottom()
{
m_boardPos = VECTOR2I();
m_boardSize = VECTOR2I();
m_boardCenter = SFVEC3F( 0.0f );
m_boardBoundingBox.Reset();
m_TH_IDs.Clear();
m_TH_ODs.Clear();
m_viaAnnuli.Clear();
m_copperLayersCount = 2;
m_biuTo3Dunits = 1.0;
m_boardBodyThickness3DU = DEFAULT_BOARD_THICKNESS * m_biuTo3Dunits;
m_frontCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_backCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_nonCopperLayerThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_frontMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_backMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_solderPasteLayerThickness3DU = SOLDERPASTE_LAYER_THICKNESS * m_biuTo3Dunits;
m_trackCount = 0;
m_viaCount = 0;
m_averageViaHoleDiameter = 0.0f;
m_holeCount = 0;
m_averageHoleDiameter = 0.0f;
m_averageTrackWidth = 0.0f;
m_BgColorBot = SFVEC4F( 0.4, 0.4, 0.5, 1.0 );
m_BgColorTop = SFVEC4F( 0.8, 0.8, 0.9, 1.0 );
m_BoardBodyColor = SFVEC4F( 0.4, 0.4, 0.5, 0.9 );
m_SolderMaskColorTop = SFVEC4F( 0.1, 0.2, 0.1, 0.83 );
m_SolderMaskColorBot = SFVEC4F( 0.1, 0.2, 0.1, 0.83 );
m_SolderPasteColor = SFVEC4F( 0.4, 0.4, 0.4, 1.0 );
m_SilkScreenColorTop = SFVEC4F( 0.9, 0.9, 0.9, 1.0 );
m_SilkScreenColorBot = SFVEC4F( 0.9, 0.9, 0.9, 1.0 );
m_CopperColor = SFVEC4F( 0.75, 0.61, 0.23, 1.0 );
m_UserDrawingsColor = SFVEC4F( 0.85, 0.85, 0.85, 1.0 );
m_UserCommentsColor = SFVEC4F( 0.85, 0.85, 0.85, 1.0 );
m_ECO1Color = SFVEC4F( 0.70, 0.10, 0.10, 1.0 );
m_ECO2Color = SFVEC4F( 0.70, 0.10, 0.10, 1.0 );
for( int ii = 0; ii < 45; ++ii )
m_UserDefinedLayerColor[ii] = SFVEC4F( 0.70, 0.10, 0.10, 1.0 );
m_platedPadsFront = nullptr;
m_platedPadsBack = nullptr;
m_offboardPadsFront = nullptr;
m_offboardPadsBack = nullptr;
m_frontPlatedCopperPolys = nullptr;
m_backPlatedCopperPolys = nullptr;
ReloadColorSettings();
g_DefaultBackgroundTop = COLOR4D( 0.80, 0.80, 0.90, 1.0 );
g_DefaultBackgroundBot = COLOR4D( 0.40, 0.40, 0.50, 1.0 );
g_DefaultSilkscreen = COLOR4D( 0.94, 0.94, 0.94, 1.0 );
g_DefaultSolderMask = COLOR4D( 0.08, 0.20, 0.14, 0.83 );
g_DefaultSolderPaste = COLOR4D( 0.50, 0.50, 0.50, 1.0 );
g_DefaultSurfaceFinish = COLOR4D( 0.75, 0.61, 0.23, 1.0 );
g_DefaultBoardBody = COLOR4D( 0.4, 0.4, 0.5, 0.9 );
g_DefaultComments = COLOR4D( 0.85, 0.85, 0.85, 1.0 );
g_DefaultECOs = COLOR4D( 0.70, 0.10, 0.10, 1.0 );
}
BOARD_ADAPTER::~BOARD_ADAPTER()
{
destroyLayers();
}
// test: frees exactly what our InitSettings allocates.
void BOARD_ADAPTER::destroyLayers()
{
for( auto& [layer, container] : m_layerMap )
delete container;
m_layerMap.clear();
for( auto& [layer, container] : m_layerHoleMap )
delete container;
m_layerHoleMap.clear();
for( auto& [layer, poly] : m_layers_poly )
delete poly;
m_layers_poly.clear();
for( auto& [layer, poly] : m_layerHoleOdPolys )
delete poly;
m_layerHoleOdPolys.clear();
for( auto& [layer, poly] : m_layerHoleIdPolys )
delete poly;
m_layerHoleIdPolys.clear();
delete m_platedPadsFront;
delete m_platedPadsBack;
delete m_offboardPadsFront;
delete m_offboardPadsBack;
m_platedPadsFront = m_platedPadsBack = nullptr;
m_offboardPadsFront = m_offboardPadsBack = nullptr;
delete m_frontPlatedCopperPolys;
delete m_backPlatedCopperPolys;
m_frontPlatedCopperPolys = nullptr;
m_backPlatedCopperPolys = nullptr;
m_TH_ODs.Clear();
m_TH_IDs.Clear();
m_viaAnnuli.Clear();
m_viaTH_ODs.Clear();
m_board_poly.RemoveAllContours();
m_TH_ODPolys.RemoveAllContours();
m_NPTH_ODPolys.RemoveAllContours();
m_viaTH_ODPolys.RemoveAllContours();
m_viaAnnuliPolys.RemoveAllContours();
}
// test: settings-free — the board-editor color table gets a neutral default
// (the real one loads COLOR_SETTINGS; scenarios don't use per-PCB-layer colors).
void BOARD_ADAPTER::ReloadColorSettings() noexcept
{
for( int layer = F_Cu; layer < PCB_LAYER_ID_COUNT; ++layer )
m_BoardEditorColors[layer] = COLOR4D( 0.75, 0.61, 0.23, 1.0 );
}
// verbatim (board_adapter.cpp:243-288) minus the m_board branches (no board).
bool BOARD_ADAPTER::Is3dLayerEnabled( PCB_LAYER_ID aLayer,
const std::bitset<LAYER_3D_END>& aVisibilityFlags ) const
{
wxASSERT( aLayer < PCB_LAYER_ID_COUNT );
switch( aLayer )
{
case B_Cu: return aVisibilityFlags.test( LAYER_3D_COPPER_BOTTOM );
case F_Cu: return aVisibilityFlags.test( LAYER_3D_COPPER_TOP );
case B_Adhes: return aVisibilityFlags.test( LAYER_3D_ADHESIVE );
case F_Adhes: return aVisibilityFlags.test( LAYER_3D_ADHESIVE );
case B_Paste: return aVisibilityFlags.test( LAYER_3D_SOLDERPASTE );
case F_Paste: return aVisibilityFlags.test( LAYER_3D_SOLDERPASTE );
case B_SilkS: return aVisibilityFlags.test( LAYER_3D_SILKSCREEN_BOTTOM );
case F_SilkS: return aVisibilityFlags.test( LAYER_3D_SILKSCREEN_TOP );
case B_Mask: return aVisibilityFlags.test( LAYER_3D_SOLDERMASK_BOTTOM );
case F_Mask: return aVisibilityFlags.test( LAYER_3D_SOLDERMASK_TOP );
case Dwgs_User: return aVisibilityFlags.test( LAYER_3D_USER_DRAWINGS );
case Cmts_User: return aVisibilityFlags.test( LAYER_3D_USER_COMMENTS );
case Eco1_User: return aVisibilityFlags.test( LAYER_3D_USER_ECO1 );
case Eco2_User: return aVisibilityFlags.test( LAYER_3D_USER_ECO2 );
default:
return false; // test: no board -> unmapped layers hidden
}
}
// test: previews/boards not modeled — footprints always "shown".
bool BOARD_ADAPTER::IsFootprintShown( const FOOTPRINT* aFootprint ) const
{
return aFootprint != nullptr;
}
// verbatim no-board branch (board_adapter.cpp:315-320).
int BOARD_ADAPTER::GetHolePlatingThickness() const noexcept
{
return DEFAULT_COPPER_THICKNESS;
}
// verbatim (board_adapter.cpp:322-328).
unsigned int BOARD_ADAPTER::GetCircleSegmentCount( float aDiameter3DU ) const
{
wxASSERT( aDiameter3DU > 0.0f );
return GetCircleSegmentCount( (int) ( aDiameter3DU / m_biuTo3Dunits ) );
}
// test: like board_adapter.cpp:330-336 with the BOARD_DESIGN_SETTINGS default
// max error (ARC_HIGH_DEF) instead of a live board's setting.
unsigned int BOARD_ADAPTER::GetCircleSegmentCount( int aDiameterBIU ) const
{
wxASSERT( aDiameterBIU > 0 );
return GetArcToSegmentCount( aDiameterBIU / 2, ARC_HIGH_DEF, FULL_CIRCLE );
}
// verbatim non-previewer path (board_adapter.cpp:808-905) minus FOLLOW_PCB
// (needs a board).
std::bitset<LAYER_3D_END> BOARD_ADAPTER::GetVisibleLayers() const
{
std::bitset<LAYER_3D_END> ret;
ret.set( LAYER_3D_BOARD, m_Cfg->m_Render.show_board_body );
ret.set( LAYER_3D_PLATED_BARRELS, m_Cfg->m_Render.show_plated_barrels );
ret.set( LAYER_3D_COPPER_TOP, m_Cfg->m_Render.show_copper_top );
ret.set( LAYER_3D_COPPER_BOTTOM, m_Cfg->m_Render.show_copper_bottom );
ret.set( LAYER_3D_SILKSCREEN_TOP, m_Cfg->m_Render.show_silkscreen_top );
ret.set( LAYER_3D_SILKSCREEN_BOTTOM, m_Cfg->m_Render.show_silkscreen_bottom );
ret.set( LAYER_3D_SOLDERMASK_TOP, m_Cfg->m_Render.show_soldermask_top );
ret.set( LAYER_3D_SOLDERMASK_BOTTOM, m_Cfg->m_Render.show_soldermask_bottom );
ret.set( LAYER_3D_SOLDERPASTE, m_Cfg->m_Render.show_solderpaste );
ret.set( LAYER_3D_ADHESIVE, m_Cfg->m_Render.show_adhesive );
ret.set( LAYER_3D_USER_COMMENTS, m_Cfg->m_Render.show_comments );
ret.set( LAYER_3D_USER_DRAWINGS, m_Cfg->m_Render.show_drawings );
ret.set( LAYER_3D_USER_ECO1, m_Cfg->m_Render.show_eco1 );
ret.set( LAYER_3D_USER_ECO2, m_Cfg->m_Render.show_eco2 );
for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
ret.set( layer, m_Cfg->m_Render.show_user[layer - LAYER_3D_USER_1] );
ret.set( LAYER_FP_REFERENCES, m_Cfg->m_Render.show_fp_references );
ret.set( LAYER_FP_VALUES, m_Cfg->m_Render.show_fp_values );
ret.set( LAYER_FP_TEXT, m_Cfg->m_Render.show_fp_text );
ret.set( LAYER_3D_TH_MODELS, m_Cfg->m_Render.show_footprints_normal );
ret.set( LAYER_3D_SMD_MODELS, m_Cfg->m_Render.show_footprints_insert );
ret.set( LAYER_3D_VIRTUAL_MODELS, m_Cfg->m_Render.show_footprints_virtual );
ret.set( LAYER_3D_MODELS_NOT_IN_POS, m_Cfg->m_Render.show_footprints_not_in_posfile );
ret.set( LAYER_3D_MODELS_MARKED_DNP, m_Cfg->m_Render.show_footprints_dnp );
ret.set( LAYER_3D_BOUNDING_BOXES, m_Cfg->m_Render.show_model_bbox );
ret.set( LAYER_3D_OFF_BOARD_SILK, m_Cfg->m_Render.show_off_board_silk );
ret.set( LAYER_3D_NAVIGATOR, m_Cfg->m_Render.show_navigator );
return ret;
}
// test: no board -> board-editor copper colors never apply.
bool BOARD_ADAPTER::GetUseBoardEditorCopperLayerColors() const
{
return false;
}
// verbatim (board_adapter.cpp:1039-1053).
float BOARD_ADAPTER::GetFootprintZPos( bool aIsFlipped ) const
{
if( aIsFlipped )
{
if( auto it = m_layerZcoordBottom.find( B_Paste ); it != m_layerZcoordBottom.end() )
return it->second;
}
else
{
if( auto it = m_layerZcoordTop.find( F_Paste ); it != m_layerZcoordTop.end() )
return it->second;
}
return 0.0;
}
// verbatim (board_adapter.cpp:1056-1070); user-layer remap dropped (unused here).
SFVEC4F BOARD_ADAPTER::GetLayerColor( int aLayerId ) const
{
wxASSERT( aLayerId < PCB_LAYER_ID_COUNT );
return GetColor( m_BoardEditorColors.at( aLayerId ) );
}
SFVEC4F BOARD_ADAPTER::GetColor( const COLOR4D& aColor ) const
{
return SFVEC4F( aColor.r, aColor.g, aColor.b, aColor.a );
}
// test: default-color scheme only (no COLOR_SETTINGS machinery).
std::map<int, COLOR4D> BOARD_ADAPTER::GetDefaultColors() const
{
std::map<int, COLOR4D> colors;
colors[LAYER_3D_BACKGROUND_TOP] = g_DefaultBackgroundTop;
colors[LAYER_3D_BACKGROUND_BOTTOM] = g_DefaultBackgroundBot;
colors[LAYER_3D_BOARD] = g_DefaultBoardBody;
colors[LAYER_3D_COPPER_TOP] = g_DefaultSurfaceFinish;
colors[LAYER_3D_COPPER_BOTTOM] = g_DefaultSurfaceFinish;
colors[LAYER_3D_SILKSCREEN_TOP] = g_DefaultSilkscreen;
colors[LAYER_3D_SILKSCREEN_BOTTOM] = g_DefaultSilkscreen;
colors[LAYER_3D_SOLDERMASK_TOP] = g_DefaultSolderMask;
colors[LAYER_3D_SOLDERMASK_BOTTOM] = g_DefaultSolderMask;
colors[LAYER_3D_SOLDERPASTE] = g_DefaultSolderPaste;
colors[LAYER_3D_USER_DRAWINGS] = g_DefaultComments;
colors[LAYER_3D_USER_COMMENTS] = g_DefaultComments;
colors[LAYER_3D_USER_ECO1] = g_DefaultECOs;
colors[LAYER_3D_USER_ECO2] = g_DefaultECOs;
return colors;
}
std::map<int, COLOR4D> BOARD_ADAPTER::GetLayerColors() const
{
return GetDefaultColors();
}
// ---------------------------------------------------------------------------
// THE SEAM: synthetic test-board data instead of a real BOARD.
//
// A 40 x 30 mm two-layer board. Layer Z stacking follows the real
// InitSettings maths (board body centered on Z=0, copper plated on top/bottom,
// tech layers above copper). All values in BIU (nm) scaled by m_biuTo3Dunits.
// ---------------------------------------------------------------------------
void BOARD_ADAPTER::InitSettings( REPORTER* aStatusReporter, REPORTER* aWarningReporter )
{
(void) aStatusReporter;
(void) aWarningReporter;
destroyLayers();
const int boardW = pcbIUScale.mmToIU( 40 );
const int boardH = pcbIUScale.mmToIU( 30 );
m_boardSize = VECTOR2I( boardW, boardH );
m_boardPos = VECTOR2I( 0, 0 );
m_copperLayersCount = 2;
// Same scale maths as the real InitSettings (board_adapter.cpp:382-387):
// no BOARD -> the "footprint holder" zoom hack applies.
m_biuTo3Dunits = RANGE_SCALE_3D / std::max( m_boardSize.x, m_boardSize.y );
m_biuTo3Dunits *= 1.6f;
m_boardBodyThickness3DU = DEFAULT_BOARD_THICKNESS * m_biuTo3Dunits;
m_frontCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_backCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_nonCopperLayerThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_frontMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_backMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_solderPasteLayerThickness3DU = SOLDERPASTE_LAYER_THICKNESS * m_biuTo3Dunits;
// Layer Z coordinates (board body spans -body/2 .. +body/2).
const float bodyTop = m_boardBodyThickness3DU / 2.0f;
const float bodyBot = -m_boardBodyThickness3DU / 2.0f;
m_layerZcoordBottom[F_Cu] = bodyTop;
m_layerZcoordTop[F_Cu] = bodyTop + m_frontCopperThickness3DU;
m_layerZcoordBottom[B_Cu] = bodyBot;
m_layerZcoordTop[B_Cu] = bodyBot - m_backCopperThickness3DU;
m_layerZcoordBottom[F_Mask] = m_layerZcoordTop[F_Cu];
m_layerZcoordTop[F_Mask] = m_layerZcoordBottom[F_Mask] + m_frontMaskThickness3DU;
m_layerZcoordBottom[B_Mask] = m_layerZcoordTop[B_Cu];
m_layerZcoordTop[B_Mask] = m_layerZcoordBottom[B_Mask] - m_backMaskThickness3DU;
m_layerZcoordBottom[F_SilkS] = m_layerZcoordTop[F_Mask];
m_layerZcoordTop[F_SilkS] = m_layerZcoordBottom[F_SilkS] + m_nonCopperLayerThickness3DU;
m_layerZcoordBottom[B_SilkS] = m_layerZcoordTop[B_Mask];
m_layerZcoordTop[B_SilkS] = m_layerZcoordBottom[B_SilkS] - m_nonCopperLayerThickness3DU;
m_layerZcoordBottom[F_Paste] = m_layerZcoordTop[F_Cu];
m_layerZcoordTop[F_Paste] = m_layerZcoordBottom[F_Paste] + m_solderPasteLayerThickness3DU;
m_layerZcoordBottom[B_Paste] = m_layerZcoordTop[B_Cu];
m_layerZcoordTop[B_Paste] = m_layerZcoordBottom[B_Paste] - m_solderPasteLayerThickness3DU;
m_boardCenter = SFVEC3F( 0.0f, 0.0f, 0.0f );
m_boardBoundingBox.Set( SFVEC3F( -boardW / 2 * m_biuTo3Dunits, -boardH / 2 * m_biuTo3Dunits,
bodyBot ),
SFVEC3F( boardW / 2 * m_biuTo3Dunits, boardH / 2 * m_biuTo3Dunits,
bodyTop ) );
// Board outline: plain rectangle.
m_board_poly.RemoveAllContours();
m_board_poly.NewOutline();
m_board_poly.Append( -boardW / 2, -boardH / 2 );
m_board_poly.Append( boardW / 2, -boardH / 2 );
m_board_poly.Append( boardW / 2, boardH / 2 );
m_board_poly.Append( -boardW / 2, boardH / 2 );
m_board_poly.Outline( 0 ).SetClosed( true );
// Copper: a few tracks + round pads per side; silkscreen: a frame; the
// BVH containers own the objects (they delete them).
auto addTrack = [&]( BVH_CONTAINER_2D* aDst, double aX1mm, double aY1mm, double aX2mm,
double aY2mm, double aWidthMm )
{
aDst->Add( new ROUND_SEGMENT_2D(
SFVEC2F( pcbIUScale.mmToIU( aX1mm ) * m_biuTo3Dunits,
pcbIUScale.mmToIU( aY1mm ) * m_biuTo3Dunits ),
SFVEC2F( pcbIUScale.mmToIU( aX2mm ) * m_biuTo3Dunits,
pcbIUScale.mmToIU( aY2mm ) * m_biuTo3Dunits ),
pcbIUScale.mmToIU( aWidthMm ) * m_biuTo3Dunits, DummyBoardItem() ) );
};
auto addCircle = [&]( BVH_CONTAINER_2D* aDst, double aXmm, double aYmm, double aRmm )
{
aDst->Add( new FILLED_CIRCLE_2D( SFVEC2F( pcbIUScale.mmToIU( aXmm ) * m_biuTo3Dunits,
pcbIUScale.mmToIU( aYmm ) * m_biuTo3Dunits ),
pcbIUScale.mmToIU( aRmm ) * m_biuTo3Dunits,
DummyBoardItem() ) );
};
BVH_CONTAINER_2D* frontCu = new BVH_CONTAINER_2D;
addTrack( frontCu, -15, -10, 15, -10, 1.0 );
addTrack( frontCu, -15, -10, -15, 10, 1.0 );
addTrack( frontCu, -15, 10, 0, 10, 0.6 );
addTrack( frontCu, 0, 10, 8, 2, 0.6 );
addCircle( frontCu, -15, -10, 1.5 );
addCircle( frontCu, 15, -10, 1.5 );
addCircle( frontCu, 8, 2, 1.2 );
frontCu->BuildBVH();
m_layerMap[F_Cu] = frontCu;
BVH_CONTAINER_2D* backCu = new BVH_CONTAINER_2D;
addTrack( backCu, 15, -10, 15, 10, 1.2 );
addTrack( backCu, 15, 10, -8, 10, 1.2 );
addCircle( backCu, -15, -10, 1.5 );
addCircle( backCu, 15, -10, 1.5 );
backCu->BuildBVH();
m_layerMap[B_Cu] = backCu;
BVH_CONTAINER_2D* frontSilk = new BVH_CONTAINER_2D;
addTrack( frontSilk, -17, -13, 17, -13, 0.3 );
addTrack( frontSilk, 17, -13, 17, 13, 0.3 );
addTrack( frontSilk, 17, 13, -17, 13, 0.3 );
addTrack( frontSilk, -17, 13, -17, -13, 0.3 );
addCircle( frontSilk, -12, 6, 0.8 );
frontSilk->BuildBVH();
m_layerMap[F_SilkS] = frontSilk;
// Through holes: the two big pads are plated through.
auto addHolePoly = [&]( SHAPE_POLY_SET& aPolys, double aXmm, double aYmm, double aRmm )
{
TransformCircleToPolygon( aPolys,
VECTOR2I( pcbIUScale.mmToIU( aXmm ), pcbIUScale.mmToIU( aYmm ) ),
pcbIUScale.mmToIU( aRmm ), ARC_HIGH_DEF, ERROR_INSIDE );
};
addCircle( &m_TH_ODs, -15, -10, 0.8 );
addCircle( &m_TH_ODs, 15, -10, 0.8 );
m_TH_ODs.BuildBVH();
addCircle( &m_TH_IDs, -15, -10, 0.65 );
addCircle( &m_TH_IDs, 15, -10, 0.65 );
m_TH_IDs.BuildBVH();
addHolePoly( m_TH_ODPolys, -15, -10, 0.8 );
addHolePoly( m_TH_ODPolys, 15, -10, 0.8 );
m_TH_ODPolys.Simplify();
}

View file

@ -0,0 +1,26 @@
// Minimal config.h for native GAL test
// Based on KiCad's generated config
#ifndef KICAD_CONFIG_H
#define KICAD_CONFIG_H
// Version info
#define KICAD_MAJOR_VERSION 8
#define KICAD_MINOR_VERSION 0
#define KICAD_PATCH_VERSION 0
// Enable OpenGL
#define KICAD_USE_OCC 0
#define KICAD_USE_EGL 0
// Platform detection
#ifdef __APPLE__
#define KICAD_MACOS 1
#endif
// Math
#define KICAD_USE_STDROUND 1
// Ensure types are properly sized
#include <cstdint>
#endif // KICAD_CONFIG_H

View file

@ -0,0 +1,146 @@
#include "fbo_capture.h"
#include <cstdio>
// Core-vs-EXT selection: Apple's 2.1 context exports the ARB entry points on
// all modern renderers, but keep the EXT fallback the compositor path proves
// works (see plan §7 risk 2). Enum values are shared between core and EXT.
static PFNGLGENFRAMEBUFFERSPROC s_glGenFramebuffers = nullptr;
static PFNGLBINDFRAMEBUFFERPROC s_glBindFramebuffer = nullptr;
static PFNGLFRAMEBUFFERTEXTURE2DPROC s_glFramebufferTexture2D = nullptr;
static PFNGLGENRENDERBUFFERSPROC s_glGenRenderbuffers = nullptr;
static PFNGLBINDRENDERBUFFERPROC s_glBindRenderbuffer = nullptr;
static PFNGLRENDERBUFFERSTORAGEPROC s_glRenderbufferStorage = nullptr;
static PFNGLFRAMEBUFFERRENDERBUFFERPROC s_glFramebufferRenderbuffer = nullptr;
static PFNGLCHECKFRAMEBUFFERSTATUSPROC s_glCheckFramebufferStatus = nullptr;
static PFNGLDELETEFRAMEBUFFERSPROC s_glDeleteFramebuffers = nullptr;
static PFNGLDELETERENDERBUFFERSPROC s_glDeleteRenderbuffers = nullptr;
static bool resolveFboEntryPoints()
{
if( s_glGenFramebuffers )
return true;
if( glad_glGenFramebuffers )
{
s_glGenFramebuffers = glad_glGenFramebuffers;
s_glBindFramebuffer = glad_glBindFramebuffer;
s_glFramebufferTexture2D = glad_glFramebufferTexture2D;
s_glGenRenderbuffers = glad_glGenRenderbuffers;
s_glBindRenderbuffer = glad_glBindRenderbuffer;
s_glRenderbufferStorage = glad_glRenderbufferStorage;
s_glFramebufferRenderbuffer = glad_glFramebufferRenderbuffer;
s_glCheckFramebufferStatus = glad_glCheckFramebufferStatus;
s_glDeleteFramebuffers = glad_glDeleteFramebuffers;
s_glDeleteRenderbuffers = glad_glDeleteRenderbuffers;
return true;
}
if( glad_glGenFramebuffersEXT )
{
std::fprintf( stderr, "[fbo] core FBO entry points missing; using EXT fallback\n" );
s_glGenFramebuffers = glad_glGenFramebuffersEXT;
s_glBindFramebuffer = glad_glBindFramebufferEXT;
s_glFramebufferTexture2D = glad_glFramebufferTexture2DEXT;
s_glGenRenderbuffers = glad_glGenRenderbuffersEXT;
s_glBindRenderbuffer = glad_glBindRenderbufferEXT;
s_glRenderbufferStorage = glad_glRenderbufferStorageEXT;
s_glFramebufferRenderbuffer = glad_glFramebufferRenderbufferEXT;
s_glCheckFramebufferStatus = glad_glCheckFramebufferStatusEXT;
s_glDeleteFramebuffers = glad_glDeleteFramebuffersEXT;
s_glDeleteRenderbuffers = glad_glDeleteRenderbuffersEXT;
return true;
}
std::fprintf( stderr, "[fbo] no framebuffer object support in this context\n" );
return false;
}
bool FBO_CAPTURE::Create( int aWidth, int aHeight )
{
if( !resolveFboEntryPoints() )
return false;
m_width = aWidth;
m_height = aHeight;
// Same sequence as EDA_3D_CANVAS::RenderToFrameBuffer (eda_3d_canvas.cpp:736-772).
s_glGenFramebuffers( 1, &m_fbo );
s_glBindFramebuffer( GL_FRAMEBUFFER, m_fbo );
glGenTextures( 1, &m_colorTexture );
glBindTexture( GL_TEXTURE_2D, m_colorTexture );
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, aWidth, aHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
s_glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
m_colorTexture, 0 );
glBindTexture( GL_TEXTURE_2D, 0 );
s_glGenRenderbuffers( 1, &m_depthStencil );
s_glBindRenderbuffer( GL_RENDERBUFFER, m_depthStencil );
s_glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, aWidth, aHeight );
s_glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER,
m_depthStencil );
const GLenum status = s_glCheckFramebufferStatus( GL_FRAMEBUFFER );
if( status != GL_FRAMEBUFFER_COMPLETE )
{
std::fprintf( stderr, "[fbo] framebuffer incomplete: 0x%04X\n", status );
Destroy();
return false;
}
return true;
}
void FBO_CAPTURE::Bind()
{
s_glBindFramebuffer( GL_FRAMEBUFFER, m_fbo );
glViewport( 0, 0, m_width, m_height );
}
bool FBO_CAPTURE::ReadPixels( std::vector<uint8_t>& aOut )
{
if( !m_fbo )
return false;
s_glBindFramebuffer( GL_FRAMEBUFFER, m_fbo );
glFinish();
aOut.resize( static_cast<size_t>( m_width ) * m_height * 4 );
glPixelStorei( GL_PACK_ALIGNMENT, 1 );
glReadPixels( 0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, aOut.data() );
const GLenum err = glGetError();
if( err != GL_NO_ERROR )
{
std::fprintf( stderr, "[fbo] glReadPixels error: 0x%04X\n", err );
return false;
}
return true;
}
void FBO_CAPTURE::Destroy()
{
if( m_fbo )
s_glDeleteFramebuffers( 1, &m_fbo );
if( m_colorTexture )
glDeleteTextures( 1, &m_colorTexture );
if( m_depthStencil )
s_glDeleteRenderbuffers( 1, &m_depthStencil );
m_fbo = m_colorTexture = m_depthStencil = 0;
}

View file

@ -0,0 +1,46 @@
/**
* Offscreen FBO for deterministic fixed-size capture, mirroring
* EDA_3D_CANVAS::RenderToFrameBuffer (eda_3d_canvas.cpp:736-772):
* GL_RGBA8 color texture + GL_DEPTH24_STENCIL8 renderbuffer on
* GL_DEPTH_STENCIL_ATTACHMENT (the stencil is required by
* OPENGL_RENDER_LIST::DrawCulled hole cutting).
*
* Rendering into an FBO makes the output independent of window size and
* Retina scaling pixels are exactly aWidth x aHeight.
*/
#ifndef FBO_CAPTURE_H
#define FBO_CAPTURE_H
#include <kicad_gl/kiglad.h>
#include <cstdint>
#include <vector>
class FBO_CAPTURE
{
public:
/// Create the FBO; returns false if the framebuffer is incomplete.
/// Falls back to the EXT entry points if the core ARB ones didn't load
/// (Apple GL 2.1 exposes both; glad loads by symbol name).
bool Create( int aWidth, int aHeight );
void Bind();
/// glFinish + glReadPixels(GL_RGBA); aOut is resized to w*h*4.
bool ReadPixels( std::vector<uint8_t>& aOut );
void Destroy();
int Width() const { return m_width; }
int Height() const { return m_height; }
private:
int m_width = 0;
int m_height = 0;
GLuint m_fbo = 0;
GLuint m_colorTexture = 0;
GLuint m_depthStencil = 0;
};
#endif // FBO_CAPTURE_H

View file

@ -0,0 +1,303 @@
/**
* Link stubs for the native 3D-renderer test build.
*
* The harness compiles real KiCad 3D-viewer TUs; the symbols those TUs
* reference from elsewhere in KiCad but only reach through dead branches
* (m_board / model-cache / app machinery is never populated here) are defined
* as safe no-ops. One stub per linker error, each annotated with the
* referencing TU. Seeded from tests/gal-regression/native/kicad_stubs.cpp.
*/
#include "kicad_stubs_3d.h"
#include <advanced_config.h>
#include <pgm_base.h>
#include <singleton.h>
#include <kicad_gl/gl_context_mgr.h>
// PGM_BASE holds unique_ptrs to these — complete types needed to define
// its ctor/dtor here.
#include <background_jobs_monitor.h>
#include <notifications_manager.h>
#include <settings/settings_manager.h>
#include <wx/snglinst.h>
#include <board.h>
#include <pad.h>
#include <pcb_track.h>
#include <libraries/library_manager.h>
#include <project_pcb.h>
#include "3d_cache/3d_cache.h"
//=============================================================================
// PGM_BASE / Pgm() — render_3d_opengl.cpp uses
// Pgm().GetGLContextManager()->RunWithoutCtxLock() during reload().
//=============================================================================
class PGM_BASE_TEST : public PGM_BASE
{
public:
PGM_BASE_TEST() { m_singleton.m_GLContextManager = new GL_CONTEXT_MANAGER(); }
void MacOpenFile( const wxString& ) override {}
};
PGM_BASE& Pgm()
{
static PGM_BASE_TEST* s_pgm = nullptr;
if( !s_pgm )
s_pgm = new PGM_BASE_TEST();
return *s_pgm;
}
// PGM_BASE out-of-line methods (pgm_base.cpp is not compiled) — same stub set
// as the GAL harness.
PGM_BASE::PGM_BASE()
{
}
PGM_BASE::~PGM_BASE()
{
}
wxApp& PGM_BASE::App()
{
static wxApp* app = nullptr;
return *app;
}
COMMON_SETTINGS* PGM_BASE::GetCommonSettings() const
{
return nullptr;
}
const wxString& PGM_BASE::GetExecutablePath() const
{
static wxString s;
return s;
}
ENV_VAR_MAP& PGM_BASE::GetLocalEnvVariables() const
{
static ENV_VAR_MAP map;
return map;
}
bool PGM_BASE::SetLanguage( wxString&, bool )
{
return false;
}
const wxString& PGM_BASE::GetTextEditor( bool )
{
static wxString s;
return s;
}
void PGM_BASE::SetTextEditor( const wxString& )
{
}
wxString PGM_BASE::GetLanguageTag()
{
return wxString();
}
void PGM_BASE::SetLanguagePath()
{
}
void PGM_BASE::ReadPdfBrowserInfos()
{
}
bool PGM_BASE::SetLocalEnvVariable( const wxString&, const wxString& )
{
return false;
}
void PGM_BASE::SetLocalEnvVariables()
{
}
void PGM_BASE::WritePdfBrowserInfos()
{
}
void PGM_BASE::SetLanguageIdentifier( int )
{
}
const wxString PGM_BASE::AskUserForPreferredEditor( const wxString& )
{
return wxString();
}
//=============================================================================
// ADVANCED_CFG singleton (advanced_config.cpp is not compiled)
//=============================================================================
const ADVANCED_CFG& ADVANCED_CFG::GetCfg()
{
static ADVANCED_CFG instance;
return instance;
}
ADVANCED_CFG::ADVANCED_CFG()
{
// Only fields the compiled TUs actually read; KiCad default DPI.
m_ScreenDPI = 91;
m_3DRT_BevelHeight_um = 30;
m_3DRT_BevelExtentFactor = 1.0 / 16.0;
}
//=============================================================================
// Profiling clock — deterministic zero for the test harness.
//=============================================================================
int64_t GetRunningMicroSecs()
{
return 0;
}
//=============================================================================
// Destructors of app-machinery members PGM_BASE owns (their real TUs would
// drag the whole settings/library world in; nothing here ever populates them).
//=============================================================================
KICAD_SINGLETON::~KICAD_SINGLETON()
{
delete m_GLContextManager;
m_GLContextManager = nullptr;
}
LIBRARY_MANAGER::~LIBRARY_MANAGER() = default;
SETTINGS_MANAGER::~SETTINGS_MANAGER() = default;
//=============================================================================
// STATUSBAR_REPORTER (reporter.cpp drags fontconfig; Redraw() takes REPORTER*
// but the harness always passes nullptr).
//=============================================================================
#include <reporter.h>
REPORTER& STATUSBAR_REPORTER::Report( const wxString&, SEVERITY )
{
return *this;
}
//=============================================================================
// BOARD / BOARD_ITEM / PAD / PCB_VIA — referenced from create_scene.cpp and
// render_3d_opengl.cpp branches that only run with a real BOARD loaded
// (m_board stays nullptr in this harness).
//=============================================================================
int BOARD::GetCopperLayerCount() const
{
return 2;
}
const EMBEDDED_FILES* BOARD::GetEmbeddedFiles() const
{
return nullptr;
}
const wxString BOARD::GetLayerName( PCB_LAYER_ID aLayer ) const
{
return LayerName( aLayer );
}
int BOARD_ITEM::GetMaxError() const
{
return ARC_HIGH_DEF;
}
bool PAD::TransformHoleToPolygon( SHAPE_POLY_SET&, int, int, ERROR_LOC ) const
{
return false;
}
int PCB_VIA::GetDrillValue() const
{
return 0;
}
void PCB_VIA::LayerPair( PCB_LAYER_ID* aTopLayer, PCB_LAYER_ID* aBottomLayer ) const
{
if( aTopLayer )
*aTopLayer = F_Cu;
if( aBottomLayer )
*aBottomLayer = B_Cu;
}
std::optional<int> PCB_VIA::GetSecondaryDrillSize() const
{
return std::nullopt;
}
std::optional<int> PCB_VIA::GetTertiaryDrillSize() const
{
return std::nullopt;
}
FILLING_MODE PCB_VIA::GetFillingMode() const
{
return static_cast<FILLING_MODE>( 0 );
}
CAPPING_MODE PCB_VIA::GetCappingMode() const
{
return static_cast<CAPPING_MODE>( 0 );
}
PLUGGING_MODE PCB_VIA::GetFrontPluggingMode() const
{
return static_cast<PLUGGING_MODE>( 0 );
}
PLUGGING_MODE PCB_VIA::GetBackPluggingMode() const
{
return static_cast<PLUGGING_MODE>( 0 );
}
COVERING_MODE PCB_VIA::GetFrontCoveringMode() const
{
return static_cast<COVERING_MODE>( 0 );
}
COVERING_MODE PCB_VIA::GetBackCoveringMode() const
{
return static_cast<COVERING_MODE>( 0 );
}
//=============================================================================
// Footprint-model loading chain (Load3dModelsIfNeeded is never called).
//=============================================================================
FOOTPRINT_LIBRARY_ADAPTER* PROJECT_PCB::FootprintLibAdapter( PROJECT* )
{
return nullptr;
}
wxString LIBRARY_MANAGER::GetFullURI( const LIBRARY_TABLE_ROW*, bool )
{
return wxString();
}
std::optional<LIBRARY_TABLE_ROW*> LIBRARY_MANAGER_ADAPTER::GetRow( const wxString&,
LIBRARY_TABLE_SCOPE ) const
{
return std::nullopt;
}
S3DMODEL* S3D_CACHE::GetModel( const wxString&, const wxString&,
std::vector<const EMBEDDED_FILES*> )
{
return nullptr;
}

View file

@ -0,0 +1,15 @@
/**
* Include shim for the native 3D-renderer test build.
* kiglad.h must precede wx/glcanvas.h so GL symbols come from the vendored
* glad loader instead of the (deprecated) Apple GL headers.
*/
#ifndef KICAD_STUBS_3D_H
#define KICAD_STUBS_3D_H
#include <kicad_gl/kiglad.h>
#include <wx/wx.h>
#include <wx/glcanvas.h>
#endif // KICAD_STUBS_3D_H

View file

@ -0,0 +1,273 @@
#include "kicad_stubs_3d.h"
#include "render3d_test_accessor.h"
#include "3d_rendering/opengl/render_3d_opengl.h"
// Member-pointer private-access technique, same as gal_test_accessor.cpp
// (https://bloglitb.blogspot.com/2010/07/access-to-private-members-thats-easy.html).
template <typename Tag>
struct result
{
typedef typename Tag::type type;
static type ptr;
};
template <typename Tag>
typename result<Tag>::type result<Tag>::ptr;
template <typename Tag, typename Tag::type p>
struct rob : result<Tag>
{
struct filler
{
filler() { result<Tag>::ptr = p; }
};
static filler filler_obj;
};
template <typename Tag, typename Tag::type p>
typename rob<Tag, p>::filler rob<Tag, p>::filler_obj;
// Tags — the typedef's signature picks the right overload of the member.
struct R3D_initializeOpenGL
{
typedef bool ( RENDER_3D_OPENGL::*type )();
};
template struct rob<R3D_initializeOpenGL, &RENDER_3D_OPENGL::initializeOpenGL>;
struct R3D_generateCylinder
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, float,
unsigned int, TRIANGLE_DISPLAY_LIST* );
};
template struct rob<R3D_generateCylinder, &RENDER_3D_OPENGL::generateCylinder>;
struct R3D_generateInvCone
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, float,
unsigned int, TRIANGLE_DISPLAY_LIST*, EDA_ANGLE );
};
template struct rob<R3D_generateInvCone, &RENDER_3D_OPENGL::generateInvCone>;
struct R3D_generateDisk
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, unsigned int,
TRIANGLE_DISPLAY_LIST*, bool );
};
template struct rob<R3D_generateDisk, &RENDER_3D_OPENGL::generateDisk>;
struct R3D_generateDimple
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, unsigned int,
TRIANGLE_DISPLAY_LIST*, bool );
};
template struct rob<R3D_generateDimple, &RENDER_3D_OPENGL::generateDimple>;
struct R3D_generateRing
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, unsigned int,
std::vector<SFVEC2F>&, std::vector<SFVEC2F>&,
bool );
};
template struct rob<R3D_generateRing, &RENDER_3D_OPENGL::generateRing>;
struct R3D_addObj_Circle
{
typedef void ( RENDER_3D_OPENGL::*type )( const FILLED_CIRCLE_2D*, TRIANGLE_DISPLAY_LIST*,
float, float );
};
template struct rob<R3D_addObj_Circle, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Ring
{
typedef void ( RENDER_3D_OPENGL::*type )( const RING_2D*, TRIANGLE_DISPLAY_LIST*, float,
float );
};
template struct rob<R3D_addObj_Ring, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Poly4
{
typedef void ( RENDER_3D_OPENGL::*type )( const POLYGON_4PT_2D*, TRIANGLE_DISPLAY_LIST*,
float, float );
};
template struct rob<R3D_addObj_Poly4, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Tri
{
typedef void ( RENDER_3D_OPENGL::*type )( const TRIANGLE_2D*, TRIANGLE_DISPLAY_LIST*, float,
float );
};
template struct rob<R3D_addObj_Tri, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Seg
{
typedef void ( RENDER_3D_OPENGL::*type )( const ROUND_SEGMENT_2D*, TRIANGLE_DISPLAY_LIST*,
float, float );
};
template struct rob<R3D_addObj_Seg, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_appendPostMachining
{
typedef bool ( RENDER_3D_OPENGL::*type )( TRIANGLE_DISPLAY_LIST*, const SFVEC2F&,
PAD_DRILL_POST_MACHINING_MODE, int, int, float,
float, bool, float, float, float* );
};
template struct rob<R3D_appendPostMachining, &RENDER_3D_OPENGL::appendPostMachiningGeometry>;
struct R3D_createBoard
{
typedef OPENGL_RENDER_LIST* ( RENDER_3D_OPENGL::*type )( const SHAPE_POLY_SET&,
const BVH_CONTAINER_2D* );
};
template struct rob<R3D_createBoard, &RENDER_3D_OPENGL::createBoard>;
struct R3D_generate3dGrid
{
typedef void ( RENDER_3D_OPENGL::*type )( GRID3D_TYPE );
};
template struct rob<R3D_generate3dGrid, &RENDER_3D_OPENGL::generate3dGrid>;
struct R3D_setupMaterials
{
typedef void ( RENDER_3D_OPENGL::*type )();
};
template struct rob<R3D_setupMaterials, &RENDER_3D_OPENGL::setupMaterials>;
struct R3D_setLayerMaterial
{
typedef void ( RENDER_3D_OPENGL::*type )( PCB_LAYER_ID );
};
template struct rob<R3D_setLayerMaterial, &RENDER_3D_OPENGL::setLayerMaterial>;
struct R3D_setArrowMaterial
{
typedef void ( RENDER_3D_OPENGL::*type )();
};
template struct rob<R3D_setArrowMaterial, &RENDER_3D_OPENGL::setArrowMaterial>;
struct R3D_m_grid
{
typedef GLuint RENDER_3D_OPENGL::*type;
};
template struct rob<R3D_m_grid, &RENDER_3D_OPENGL::m_grid>;
// ---- public wrappers ----
bool R3D_InitializeOpenGL( RENDER_3D_OPENGL& aRenderer )
{
return ( aRenderer.*result<R3D_initializeOpenGL>::ptr )();
}
void R3D_GenerateCylinder( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst )
{
( aRenderer.*result<R3D_generateCylinder>::ptr )( aCenter, aInnerRadius, aOuterRadius, aZtop,
aZbot, aNrSides, aDst );
}
void R3D_GenerateInvCone( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, EDA_ANGLE aAngle )
{
( aRenderer.*result<R3D_generateInvCone>::ptr )( aCenter, aInnerRadius, aOuterRadius, aZtop,
aZbot, aNrSides, aDst, aAngle );
}
void R3D_GenerateDisk( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, bool aTop )
{
( aRenderer.*result<R3D_generateDisk>::ptr )( aCenter, aRadius, aZ, aNrSides, aDst, aTop );
}
void R3D_GenerateDimple( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, float aDepth, unsigned int aNrSides,
TRIANGLE_DISPLAY_LIST* aDst, bool aTop )
{
( aRenderer.*result<R3D_generateDimple>::ptr )( aCenter, aRadius, aZ, aDepth, aNrSides, aDst,
aTop );
}
void R3D_GenerateRing( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aInnerRadius,
float aOuterRadius, unsigned int aNrSides,
std::vector<SFVEC2F>& aInnerContour, std::vector<SFVEC2F>& aOuterContour,
bool aInvertOrder )
{
( aRenderer.*result<R3D_generateRing>::ptr )( aCenter, aInnerRadius, aOuterRadius, aNrSides,
aInnerContour, aOuterContour, aInvertOrder );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const FILLED_CIRCLE_2D* aCircle,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Circle>::ptr )( aCircle, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const RING_2D* aRing,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Ring>::ptr )( aRing, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const POLYGON_4PT_2D* aPoly,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Poly4>::ptr )( aPoly, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const TRIANGLE_2D* aTri,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Tri>::ptr )( aTri, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const ROUND_SEGMENT_2D* aSeg,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Seg>::ptr )( aSeg, aDst, aZtop, aZbot );
}
bool R3D_AppendPostMachining( RENDER_3D_OPENGL& aRenderer, TRIANGLE_DISPLAY_LIST* aDst,
const SFVEC2F& aHoleCenter, PAD_DRILL_POST_MACHINING_MODE aMode,
int aSizeIU, int aDepthIU, float aHoleInnerRadius, float aZSurface,
bool aIsFront, float aPlatingThickness3d, float aUnitScale,
float* aZEnd )
{
return ( aRenderer.*result<R3D_appendPostMachining>::ptr )(
aDst, aHoleCenter, aMode, aSizeIU, aDepthIU, aHoleInnerRadius, aZSurface, aIsFront,
aPlatingThickness3d, aUnitScale, aZEnd );
}
OPENGL_RENDER_LIST* R3D_CreateBoard( RENDER_3D_OPENGL& aRenderer,
const SHAPE_POLY_SET& aBoardPoly,
const BVH_CONTAINER_2D* aThroughHoles )
{
return ( aRenderer.*result<R3D_createBoard>::ptr )( aBoardPoly, aThroughHoles );
}
void R3D_Generate3dGrid( RENDER_3D_OPENGL& aRenderer, GRID3D_TYPE aGridType )
{
( aRenderer.*result<R3D_generate3dGrid>::ptr )( aGridType );
}
unsigned int R3D_GetGridList( RENDER_3D_OPENGL& aRenderer )
{
return aRenderer.*result<R3D_m_grid>::ptr;
}
void R3D_SetupMaterials( RENDER_3D_OPENGL& aRenderer )
{
( aRenderer.*result<R3D_setupMaterials>::ptr )();
}
void R3D_SetLayerMaterial( RENDER_3D_OPENGL& aRenderer, PCB_LAYER_ID aLayerID )
{
( aRenderer.*result<R3D_setLayerMaterial>::ptr )( aLayerID );
}
void R3D_SetArrowMaterial( RENDER_3D_OPENGL& aRenderer )
{
( aRenderer.*result<R3D_setArrowMaterial>::ptr )();
}

View file

@ -0,0 +1,83 @@
/**
* Access to RENDER_3D_OPENGL's private geometry generators / material setters
* for the Tier-2 scenarios, using the same member-pointer technique as
* tests/gal-regression/native/gal_test_accessor.cpp (no KiCad header edits).
* Overloads are disambiguated by the tag's member-function-pointer typedef.
*/
#ifndef RENDER3D_TEST_ACCESSOR_H
#define RENDER3D_TEST_ACCESSOR_H
#include <plugins/3dapi/xv3d_types.h>
#include <3d_enums.h>
#include <geometry/eda_angle.h>
#include <layer_ids.h>
#include <padstack.h> // PAD_DRILL_POST_MACHINING_MODE
#include <vector>
class RENDER_3D_OPENGL;
class TRIANGLE_DISPLAY_LIST;
class OPENGL_RENDER_LIST;
class FILLED_CIRCLE_2D;
class RING_2D;
class POLYGON_4PT_2D;
class TRIANGLE_2D;
class ROUND_SEGMENT_2D;
class SHAPE_POLY_SET;
class BVH_CONTAINER_2D;
bool R3D_InitializeOpenGL( RENDER_3D_OPENGL& aRenderer );
void R3D_GenerateCylinder( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst );
void R3D_GenerateInvCone( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, EDA_ANGLE aAngle );
void R3D_GenerateDisk( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, bool aTop );
void R3D_GenerateDimple( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, float aDepth, unsigned int aNrSides,
TRIANGLE_DISPLAY_LIST* aDst, bool aTop );
void R3D_GenerateRing( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aInnerRadius,
float aOuterRadius, unsigned int aNrSides,
std::vector<SFVEC2F>& aInnerContour, std::vector<SFVEC2F>& aOuterContour,
bool aInvertOrder );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const FILLED_CIRCLE_2D* aCircle,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const RING_2D* aRing,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const POLYGON_4PT_2D* aPoly,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const TRIANGLE_2D* aTri,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const ROUND_SEGMENT_2D* aSeg,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
bool R3D_AppendPostMachining( RENDER_3D_OPENGL& aRenderer, TRIANGLE_DISPLAY_LIST* aDst,
const SFVEC2F& aHoleCenter, PAD_DRILL_POST_MACHINING_MODE aMode,
int aSizeIU, int aDepthIU, float aHoleInnerRadius, float aZSurface,
bool aIsFront, float aPlatingThickness3d, float aUnitScale,
float* aZEnd );
OPENGL_RENDER_LIST* R3D_CreateBoard( RENDER_3D_OPENGL& aRenderer,
const SHAPE_POLY_SET& aBoardPoly,
const BVH_CONTAINER_2D* aThroughHoles = nullptr );
void R3D_Generate3dGrid( RENDER_3D_OPENGL& aRenderer, GRID3D_TYPE aGridType );
/// The compiled grid display-list id (private m_grid).
unsigned int R3D_GetGridList( RENDER_3D_OPENGL& aRenderer );
void R3D_SetupMaterials( RENDER_3D_OPENGL& aRenderer );
void R3D_SetLayerMaterial( RENDER_3D_OPENGL& aRenderer, PCB_LAYER_ID aLayerID );
void R3D_SetArrowMaterial( RENDER_3D_OPENGL& aRenderer );
#endif // RENDER3D_TEST_ACCESSOR_H

View file

@ -0,0 +1,299 @@
/**
* Native 3D-renderer test application golden-baseline generator.
*
* Renders the shared scenarios (tests/3d-regression/scenarios/) through the
* REAL KiCad 3D-viewer OpenGL code on a desktop GL 2.1 compatibility context,
* captures each into a fixed-size offscreen FBO and writes PNGs. These PNGs
* are the committed goldens the WebGL port will be compared against with the
* pixelmatch engine (tests/tools/screenshots/compare-dirs.ts).
*
* Modeled on tests/gal-regression/native/gal_native_test.cpp.
*/
#include "kicad_stubs_3d.h" // kiglad before wx
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include "fbo_capture.h"
#include "scene3d_test_ctx.h"
#include "scene3d_test_scenarios.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
// Fixed capture size — must match the WebGL harness canvas (wasm/3d_webgl_test.html)
// and manifest.json. FBO capture makes this independent of window size / Retina.
static const int CAPTURE_WIDTH = 800;
static const int CAPTURE_HEIGHT = 600;
static std::string g_outputDir;
static std::string g_manifestPath;
static std::string g_filter;
static bool g_showWindow = false;
static bool SavePng( const std::string& aPath, std::vector<uint8_t>& aPixels, int aWidth,
int aHeight )
{
// Force alpha opaque: the browser canvas the WebGL side screenshots is
// composited opaque, while the FBO keeps partial alpha (same rationale as
// gal_native_test.cpp SaveScreenshot).
for( size_t i = 3; i < aPixels.size(); i += 4 )
aPixels[i] = 255;
// OpenGL rows are bottom-up.
stbi_flip_vertically_on_write( 1 );
return stbi_write_png( aPath.c_str(), aWidth, aHeight, 4, aPixels.data(), aWidth * 4 ) != 0;
}
static bool WriteManifest( const std::string& aPath )
{
std::ofstream out( aPath );
if( !out )
{
std::cerr << "Failed to write manifest: " << aPath << "\n";
return false;
}
out << "{\n \"width\": " << CAPTURE_WIDTH << ",\n \"height\": " << CAPTURE_HEIGHT
<< ",\n \"scenarios\": [\n";
for( int i = 0; i < Scene3DTest::GetScenarioCount(); i++ )
{
out << " \"" << Scene3DTest::GetScenarioName( i ) << "\""
<< ( i + 1 < Scene3DTest::GetScenarioCount() ? "," : "" ) << "\n";
}
out << " ]\n}\n";
return true;
}
class TEST_GL_CANVAS : public wxGLCanvas
{
public:
explicit TEST_GL_CANVAS( wxWindow* aParent, const wxGLAttributes& aAttrs ) :
wxGLCanvas( aParent, aAttrs, wxID_ANY, wxDefaultPosition,
wxSize( CAPTURE_WIDTH, CAPTURE_HEIGHT ) )
{
wxGLContextAttrs ctxAttrs; // default: legacy compatibility profile —
ctxAttrs.PlatformDefaults().EndList(); // required for immediate mode + display lists
m_context = new wxGLContext( this, nullptr, &ctxAttrs );
}
~TEST_GL_CANVAS() override { delete m_context; }
bool MakeCurrent() { return SetCurrent( *m_context ); }
private:
wxGLContext* m_context;
};
class SCENE3D_TEST_FRAME : public wxFrame
{
public:
SCENE3D_TEST_FRAME() :
wxFrame( nullptr, wxID_ANY, "3D Renderer Native Test", wxDefaultPosition,
wxSize( CAPTURE_WIDTH, CAPTURE_HEIGHT ) )
{
wxGLAttributes attrs;
attrs.PlatformDefaults().RGBA().DoubleBuffer().Depth( 24 ).Stencil( 8 ).EndList();
m_canvas = new TEST_GL_CANVAS( this, attrs );
CallAfter( &SCENE3D_TEST_FRAME::RunScenarios );
}
void RunScenarios()
{
std::vector<int> toRun;
for( int i = 0; i < Scene3DTest::GetScenarioCount(); i++ )
{
const std::string name = Scene3DTest::GetScenarioName( i );
if( g_filter.empty() || name.find( g_filter ) != std::string::npos )
toRun.push_back( i );
}
m_total = static_cast<int>( toRun.size() );
if( !m_canvas->MakeCurrent() )
{
std::cerr << "Failed to make GL context current\n";
Close();
return;
}
const int gladVersion = gladLoaderLoadGL();
if( !gladVersion )
{
std::cerr << "gladLoaderLoadGL failed\n";
Close();
return;
}
std::cout << "GL_VERSION: " << (const char*) glGetString( GL_VERSION ) << "\n";
std::cout << "GL_RENDERER: " << (const char*) glGetString( GL_RENDERER ) << "\n";
// The FFP renderer needs a compatibility context: display lists must exist.
if( !glad_glGenLists )
{
std::cerr << "glGenLists did not load — not a compatibility context?\n";
Close();
return;
}
FBO_CAPTURE fbo;
if( !fbo.Create( CAPTURE_WIDTH, CAPTURE_HEIGHT ) )
{
std::cerr << "FBO creation failed\n";
Close();
return;
}
fs::create_directories( g_outputDir );
SCENE3D_CTX ctx( CAPTURE_WIDTH, CAPTURE_HEIGHT );
ctx.InitOnce(); // initializeOpenGL()-equivalent state + circle texture
std::vector<uint8_t> pixels;
for( int i : toRun )
{
const std::string name = Scene3DTest::GetScenarioName( i );
std::cout << "Scenario " << i << ": " << name << "... " << std::flush;
fbo.Bind();
Scene3DTest::RenderScenario( ctx, i );
const std::string path = g_outputDir + "/3d-" + name + ".png";
if( fbo.ReadPixels( pixels ) && SavePng( path, pixels, fbo.Width(), fbo.Height() ) )
{
std::cout << "OK\n";
m_passed++;
}
else
{
std::cout << "FAILED\n";
}
}
fbo.Destroy();
if( !g_manifestPath.empty() )
{
if( WriteManifest( g_manifestPath ) )
std::cout << "Manifest: " << g_manifestPath << "\n";
else
m_passed = -1;
}
std::cout << "\nResults: " << m_passed << "/" << m_total << " scenarios saved\n";
if( !g_showWindow )
Close();
}
int GetPassed() const { return m_passed; }
int GetTotal() const { return m_total; }
private:
TEST_GL_CANVAS* m_canvas;
int m_passed = 0;
int m_total = 0;
};
class SCENE3D_TEST_APP : public wxApp
{
public:
bool OnInit() override
{
m_frame = new SCENE3D_TEST_FRAME();
m_frame->Show( true );
return true;
}
int OnExit() override
{
if( m_frame )
return ( m_frame->GetPassed() == m_frame->GetTotal() ) ? 0 : 1;
return 1;
}
private:
SCENE3D_TEST_FRAME* m_frame = nullptr;
};
wxIMPLEMENT_APP_NO_MAIN( SCENE3D_TEST_APP );
int main( int argc, char** argv )
{
for( int i = 1; i < argc; i++ )
{
const std::string arg = argv[i];
if( arg == "--output" && i + 1 < argc )
{
g_outputDir = argv[++i];
}
else if( arg == "--manifest" && i + 1 < argc )
{
g_manifestPath = argv[++i];
}
else if( arg == "--filter" && i + 1 < argc )
{
g_filter = argv[++i];
}
else if( arg == "--show" )
{
g_showWindow = true;
}
else if( arg == "--list" )
{
for( int s = 0; s < Scene3DTest::GetScenarioCount(); s++ )
std::cout << s << ": " << Scene3DTest::GetScenarioName( s ) << "\n";
return 0;
}
else
{
std::cout << "Usage: scene3d_native_test --output <dir> [options]\n"
" --output <dir> Output directory for 3d-<name>.png\n"
" --manifest <file> Write the scenario manifest JSON\n"
" --filter <substr> Only run scenarios whose name contains <substr>\n"
" --list Print scenario names and exit\n"
" --show Keep the window open after rendering\n";
return arg == "--help" ? 0 : 2;
}
}
if( g_outputDir.empty() )
{
std::cerr << "--output is required (or use --list)\n";
return 2;
}
std::cout << "3D Renderer Native Test - RENDER_3D_OPENGL Baseline Generator\n";
std::cout << "==============================================================\n\n";
return wxEntry( argc, argv );
}

View file

@ -0,0 +1,159 @@
/**
* Test-harness stub of the EDA_3D_VIEWER_SETTINGS / APP_SETTINGS_BASE /
* JSON_SETTINGS chain. The renderer only reads m_Render/m_Camera plain
* fields; none of the JSON load/store machinery ever runs, so every virtual
* is a no-op and the ctor fills the render settings with the upstream
* defaults (deterministic test values, documented deviations flagged).
*/
#include "kicad_stubs_3d.h"
#include "3d_viewer/eda_3d_viewer_settings.h"
#include "common_ogl/ogl_attr_list.h" // ANTIALIASING_MODE (fwd-declared in the settings header)
#include <settings/json_settings_internals.h>
// ---- JSON_SETTINGS ----
JSON_SETTINGS::JSON_SETTINGS( const wxString& aFilename, SETTINGS_LOC aLocation,
int aSchemaVersion, bool aCreateIfMissing, bool aCreateIfDefault,
bool aWriteFile ) :
m_filename( aFilename ),
m_legacy_filename( "" ),
m_location( aLocation ),
m_createIfMissing( aCreateIfMissing ),
m_createIfDefault( aCreateIfDefault ),
m_writeFile( aWriteFile ),
m_modified( false ),
m_deleteLegacyAfterMigration( false ),
m_resetParamsIfMissing( true ),
m_schemaVersion( aSchemaVersion ),
m_manager( nullptr )
{
m_internals = std::make_unique<JSON_SETTINGS_INTERNALS>();
}
JSON_SETTINGS::~JSON_SETTINGS() = default;
void JSON_SETTINGS::Load() {}
bool JSON_SETTINGS::Store() { return false; }
bool JSON_SETTINGS::LoadFromFile( const wxString& ) { return false; }
bool JSON_SETTINGS::SaveToFile( const wxString&, bool ) { return false; }
std::map<std::string, nlohmann::json> JSON_SETTINGS::GetFileHistories() { return {}; }
bool JSON_SETTINGS::MigrateFromLegacy( wxConfigBase* ) { return false; }
// ---- APP_SETTINGS_BASE ----
APP_SETTINGS_BASE::APP_SETTINGS_BASE( const std::string& aFilename, int aSchemaVersion ) :
JSON_SETTINGS( aFilename, SETTINGS_LOC::USER, aSchemaVersion, true, true, true ),
m_CrossProbing(),
m_FindReplace(),
m_Graphics(),
m_ColorPicker(),
m_LibTree(),
m_Printing(),
m_SearchPane(),
m_System(),
m_Window(),
m_appSettingsSchemaVersion( aSchemaVersion )
{
}
bool APP_SETTINGS_BASE::MigrateFromLegacy( wxConfigBase* ) { return false; }
// ---- EDA_3D_VIEWER_SETTINGS ----
EDA_3D_VIEWER_SETTINGS::EDA_3D_VIEWER_SETTINGS() :
APP_SETTINGS_BASE( "3d_viewer", 0 ),
m_Render(),
m_Camera()
{
RENDER_SETTINGS& r = m_Render;
// Upstream defaults (eda_3d_viewer_settings.cpp PARAM defaults), with two
// determinism-driven deviations: engine is OPENGL (the renderer under
// test) and AA is NONE (context is single-sample anyway).
r.engine = RENDER_ENGINE::OPENGL;
r.grid_type = GRID3D_TYPE::NONE;
r.opengl_AA_mode = ANTIALIASING_MODE::AA_NONE;
r.material_mode = MATERIAL_MODE::NORMAL;
r.opengl_AA_disableOnMove = false;
r.opengl_thickness_disableOnMove = false;
r.opengl_microvias_disableOnMove = false;
r.opengl_holes_disableOnMove = false;
r.opengl_render_bbox_only_OnMove = false;
r.opengl_copper_thickness = true;
r.show_model_bbox = false;
r.show_off_board_silk = false;
r.highlight_on_rollover = false;
r.opengl_selection_color = KIGFX::COLOR4D( 0.0, 1.0, 0.0, 1.0 );
r.raytrace_anti_aliasing = false;
r.raytrace_backfloor = false;
r.raytrace_post_processing = false;
r.raytrace_procedural_textures = false;
r.raytrace_reflections = false;
r.raytrace_refractions = false;
r.raytrace_shadows = false;
r.raytrace_nrsamples_shadows = 0;
r.raytrace_nrsamples_reflections = 0;
r.raytrace_nrsamples_refractions = 0;
r.raytrace_spread_shadows = 0.0f;
r.raytrace_spread_reflections = 0.0f;
r.raytrace_spread_refractions = 0.0f;
r.raytrace_recursivelevel_reflections = 0;
r.raytrace_recursivelevel_refractions = 0;
r.raytrace_lightColorCamera = KIGFX::COLOR4D( 0.2, 0.2, 0.2, 1.0 );
r.raytrace_lightColorTop = KIGFX::COLOR4D( 0.247, 0.247, 0.247, 1.0 );
r.raytrace_lightColorBottom = KIGFX::COLOR4D( 0.247, 0.247, 0.247, 1.0 );
r.show_adhesive = true;
r.show_navigator = false;
r.show_board_body = true;
r.show_plated_barrels = true;
r.show_comments = true;
r.show_drawings = true;
r.show_eco1 = true;
r.show_eco2 = true;
for( bool& user : r.show_user )
user = false;
r.show_footprints_insert = true;
r.show_footprints_normal = true;
r.show_footprints_virtual = true;
r.show_footprints_not_in_posfile = true;
r.show_footprints_dnp = true;
r.show_silkscreen_top = true;
r.show_silkscreen_bottom = true;
r.show_soldermask_top = true;
r.show_soldermask_bottom = true;
r.show_solderpaste = true;
r.show_copper_top = true;
r.show_copper_bottom = true;
r.show_zones = true;
r.show_fp_references = true;
r.show_fp_values = true;
r.show_fp_text = true;
r.subtract_mask_from_silk = false;
r.clip_silk_on_via_annuli = true;
r.differentiate_plated_copper = true;
r.use_board_editor_copper_colors = false;
r.preview_show_board_body = true;
m_Camera.animation_enabled = false;
m_Camera.moving_speed_multiplier = 3;
m_Camera.rotation_increment = 10.0;
m_Camera.projection_mode = 0;
}
LAYER_PRESET_3D* EDA_3D_VIEWER_SETTINGS::FindPreset( const wxString& )
{
return nullptr;
}
bool EDA_3D_VIEWER_SETTINGS::MigrateFromLegacy( wxConfigBase* )
{
return false;
}

File diff suppressed because it is too large Load diff