diff --git a/tests/apps/gal-webgl/gal_webgl_test.html b/tests/apps/gal-webgl/gal_webgl_test.html index 49488c5..a0d3013 100644 --- a/tests/apps/gal-webgl/gal_webgl_test.html +++ b/tests/apps/gal-webgl/gal_webgl_test.html @@ -1,60 +1,84 @@ - + GAL WebGL Test -

GAL WebGL Test

+ +
-
- -
+ +
-
+ +
- - -
- -
-
Loading WASM module...
+ + +
Loading WASM...
- + diff --git a/tests/e2e/gal-webgl.spec.ts b/tests/e2e/gal-webgl.spec.ts index 9956033..23d0174 100644 --- a/tests/e2e/gal-webgl.spec.ts +++ b/tests/e2e/gal-webgl.spec.ts @@ -96,9 +96,38 @@ test.describe('GAL WebGL Regression Tests', () => { // Wait for rendering to complete await page.waitForTimeout(100); - // Get the canvas element and take a screenshot of just the canvas - const canvas = await page.locator('#canvas'); - await expect(canvas).toBeVisible(); + // Debug: list all canvases on the page + const canvasInfo = await page.evaluate(() => { + const canvases = document.querySelectorAll('canvas'); + const windowContainer = document.getElementById('window-container'); + return { + canvases: Array.from(canvases).map(c => ({ + id: c.id, + className: c.className, + width: c.width, + height: c.height, + display: window.getComputedStyle(c).display, + parentId: c.parentElement?.id + })), + windowContainerChildren: windowContainer?.children.length || 0, + allElements: document.querySelectorAll('#window-container *').length + }; + }); + console.log('Canvas debug:', JSON.stringify(canvasInfo, null, 2)); + + // Get the canvas element + // First try GL canvas, fall back to main canvas + let canvas = page.locator('.gl-canvas').first(); + if (!(await canvas.count())) { + canvas = page.locator('#canvas'); + } + + // If still no canvas, use first available + if (!(await canvas.count())) { + canvas = page.locator('canvas').first(); + } + + await expect(canvas).toBeVisible({ timeout: 5000 }); // Screenshot the canvas (matching native 800x600 output) const screenshotPath = path.join(OUTPUT_DIR, `gal-${scenarioName}.png`); diff --git a/tests/gal-regression/wasm/Makefile b/tests/gal-regression/wasm/Makefile index 2d0bb81..e98bb8e 100644 --- a/tests/gal-regression/wasm/Makefile +++ b/tests/gal-regression/wasm/Makefile @@ -29,7 +29,9 @@ SYSROOT = $(PROJECT_ROOT)/build-wasm/sysroot # KiCad include paths # Note: -I../native provides stub files (config.h, kicad_stubs.h, etc.) +# Note: -I../scenarios provides shared test scenarios (gal_test_scenarios.h) KICAD_INCLUDES = -I../native \ + -I../scenarios \ -Igenerated \ -I$(KICAD_ROOT)/include \ -I$(KICAD_ROOT)/include/gal \ @@ -76,7 +78,18 @@ JS = $(TOOLS_ROOT)/wx.js HTML_TEMPLATE = $(TOOLS_ROOT)/template.html # Source files -MAIN_SRCS = gal_webgl_test.cpp +MAIN_SRCS = gal_webgl_test.cpp wasm_stubs.cpp + +# KiCad sources needed for linking (not in WEBGL_SRCS) +KICAD_SRCS = $(KICAD_ROOT)/common/gal/hidpi_gl_canvas.cpp \ + $(KICAD_ROOT)/common/gal/graphics_abstraction_layer.cpp \ + $(KICAD_ROOT)/common/gal/gal_display_options.cpp + +# Test scenario sources (shared with native test) +SCENARIO_SRCS = ../scenarios/gal_test_scenarios.cpp + +# GLU tesselator - stub (no actual implementation needed for basic shapes) +GLU_SRCS = # Generated shader sources (from generate_shaders.py) SHADER_SRCS = generated/glsl_kicad_frag.cpp \ @@ -107,7 +120,7 @@ WEBGL_SRCS = webgl/webgl_gal.cpp \ webgl/gl_resources.cpp \ webgl/webgl_antialiasing.cpp -SRCS = $(MAIN_SRCS) $(SHADER_SRCS) $(WEBGL_SRCS) +SRCS = $(MAIN_SRCS) $(KICAD_SRCS) $(SCENARIO_SRCS) $(GLU_SRCS) $(SHADER_SRCS) $(WEBGL_SRCS) OBJS = $(SRCS:.cpp=.o) # Target diff --git a/tests/gal-regression/wasm/gal_webgl_test.cpp b/tests/gal-regression/wasm/gal_webgl_test.cpp index 91f4969..5396c19 100644 --- a/tests/gal-regression/wasm/gal_webgl_test.cpp +++ b/tests/gal-regression/wasm/gal_webgl_test.cpp @@ -1,87 +1,70 @@ /** * GAL WebGL Test - WASM Entry Point * - * This is a test harness that renders GAL test scenarios using WEBGL_GAL + * This test harness renders GAL test scenarios using WEBGL_GAL * and allows Playwright to capture screenshots for comparison against native. - * - * Phase 1: Stub that initializes WebGL and renders a test pattern - * Phase 2: Full WEBGL_GAL implementation with all scenarios */ +// Include kiglew.h first to set GLEW guard before anything includes +#include "webgl/kiglew.h" + +#include +#include + #include #include -#include + +// KiCad GAL headers +#include +#include +#include "webgl/webgl_gal.h" + +// Test scenarios (shared with native test) +#include "gal_test_scenarios.h" + +// Stubs +#include "kicad_stubs.h" + #include -#include // Canvas dimensions (matching native test) static const int CANVAS_WIDTH = 800; static const int CANVAS_HEIGHT = 600; -// Current scenario index +// Global state static int g_currentScenario = -1; static int g_totalScenarios = 28; - -// WebGL context -static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_glContext = 0; +static KIGFX::WEBGL_GAL* g_gal = nullptr; +static wxFrame* g_frame = nullptr; /** - * Initialize WebGL context + * Render the current scenario */ -bool initWebGL() { - EmscriptenWebGLContextAttributes attrs; - emscripten_webgl_init_context_attributes(&attrs); - - attrs.majorVersion = 2; // WebGL 2.0 - attrs.minorVersion = 0; - attrs.alpha = true; - attrs.depth = true; - attrs.stencil = true; - attrs.antialias = false; // We handle AA ourselves - attrs.premultipliedAlpha = false; - attrs.preserveDrawingBuffer = true; // Needed for screenshots - - g_glContext = emscripten_webgl_create_context("#canvas", &attrs); - if (g_glContext <= 0) { - printf("ERROR: Failed to create WebGL 2.0 context: %d\n", g_glContext); - return false; +void renderCurrentScenario() { + if (!g_gal || g_currentScenario < 0) { + return; } - emscripten_webgl_make_context_current(g_glContext); + printf("Rendering scenario %d: %s\n", g_currentScenario, + GALTest::GetScenarioName(g_currentScenario)); - printf("WebGL 2.0 context created successfully\n"); - printf(" GL_VENDOR: %s\n", glGetString(GL_VENDOR)); - printf(" GL_RENDERER: %s\n", glGetString(GL_RENDERER)); - printf(" GL_VERSION: %s\n", glGetString(GL_VERSION)); + // Begin drawing + g_gal->BeginDrawing(); - return true; + // Clear the screen + g_gal->ClearScreen(); + + // Render the scenario + GALTest::RenderScenario(g_gal, g_currentScenario, CANVAS_WIDTH, CANVAS_HEIGHT); + + // End drawing and present + g_gal->EndDrawing(); + + printf("Scenario %d rendered\n", g_currentScenario); } /** - * Render a test pattern (stub for Phase 1) - * In Phase 2, this will be replaced with actual WEBGL_GAL rendering - */ -void renderTestPattern(int scenarioIndex) { - // Set viewport - glViewport(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); - - // Clear with KiCad-like dark background - glClearColor(0.102f, 0.102f, 0.149f, 1.0f); // RGB(26, 26, 38) - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - // TODO Phase 2: Replace with actual WEBGL_GAL rendering - // For now, just render a different colored rectangle for each scenario - // to verify the pipeline works - - // This is a placeholder - in Phase 2 we'll call: - // GALTest::RenderScenario(webglGal, scenarioIndex, CANVAS_WIDTH, CANVAS_HEIGHT); - - printf("Rendered scenario %d (stub)\n", scenarioIndex); -} - -/** - * Run a specific scenario - * Called from JavaScript: Module.ccall('runScenario', 'number', ['number'], [index]) + * C API for JavaScript calls */ extern "C" { @@ -94,7 +77,7 @@ int runScenario(int scenarioIndex) { } g_currentScenario = scenarioIndex; - renderTestPattern(scenarioIndex); + renderCurrentScenario(); return 0; } @@ -122,24 +105,84 @@ int getCanvasHeight() { } // extern "C" /** - * Main entry point + * wxWidgets Application */ -int main() { - printf("GAL WebGL Test - Phase 1 Stub\n"); - printf("============================\n\n"); +class GALTestApp : public wxApp { +public: + virtual bool OnInit() override; +}; - // Set canvas size - emscripten_set_canvas_element_size("#canvas", CANVAS_WIDTH, CANVAS_HEIGHT); +wxIMPLEMENT_APP_NO_MAIN(GALTestApp); - // Initialize WebGL - if (!initWebGL()) { - printf("Failed to initialize WebGL\n"); - return 1; +bool GALTestApp::OnInit() { + printf("GAL WebGL Test - Initializing wxWidgets\n"); + + // Create a frame (invisible, just hosts the GL canvas) + g_frame = new wxFrame(nullptr, wxID_ANY, "GAL WebGL Test", + wxDefaultPosition, wxSize(CANVAS_WIDTH, CANVAS_HEIGHT)); + + // Create display options + KIGFX::GAL_DISPLAY_OPTIONS displayOptions; + KIGFX::VC_SETTINGS vcSettings; + + printf("Creating WEBGL_GAL instance...\n"); + + try { + g_gal = new KIGFX::WEBGL_GAL( + vcSettings, + displayOptions, + g_frame, // parent window + nullptr, // mouse listener + nullptr, // paint listener + "GAL WebGL Test" // name + ); + } catch (const std::exception& e) { + printf("ERROR: Failed to create WEBGL_GAL: %s\n", e.what()); + return false; } + printf("WEBGL_GAL created successfully\n"); + + // Set up the GAL + g_gal->SetScreenSize(VECTOR2I(CANVAS_WIDTH, CANVAS_HEIGHT)); + g_gal->ResizeScreen(CANVAS_WIDTH, CANVAS_HEIGHT); + + // Set default world coordinates (matching native test) + double worldScale = 1.0 / 10000.0; // Convert nm to screen units + g_gal->SetWorldUnitLength(worldScale); + g_gal->SetScreenDPI(96); + + // Set world boundaries + VECTOR2D worldSize(CANVAS_WIDTH / worldScale, CANVAS_HEIGHT / worldScale); + BOX2D worldBounds(VECTOR2D(0, 0), worldSize); + g_gal->SetWorldScreenMatrix(g_gal->GetWorldScreenMatrix()); + + // Initialize the compositor + g_gal->BeginDrawing(); + g_gal->ClearScreen(); + g_gal->EndDrawing(); + printf("\nReady for scenarios. Total: %d\n", g_totalScenarios); printf("Call runScenario(index) from JavaScript to render.\n"); + return true; +} + +/** + * Main entry point + */ +int main(int argc, char* argv[]) { + printf("GAL WebGL Test\n"); + printf("==============\n\n"); + + // Initialize wxWidgets + wxEntryStart(argc, argv); + + if (!wxTheApp->OnInit()) { + printf("ERROR: wxApp initialization failed\n"); + return 1; + } + // Don't exit - keep runtime alive for JavaScript calls emscripten_exit_with_live_runtime(); diff --git a/tests/gal-regression/wasm/gal_webgl_test.html b/tests/gal-regression/wasm/gal_webgl_test.html index 49488c5..a0d3013 100644 --- a/tests/gal-regression/wasm/gal_webgl_test.html +++ b/tests/gal-regression/wasm/gal_webgl_test.html @@ -1,60 +1,84 @@ - + GAL WebGL Test -

GAL WebGL Test

+ +
-
- -
+ +
-
+ +
- - -
- -
-
Loading WASM module...
+ + +
Loading WASM...
- + diff --git a/tests/gal-regression/wasm/wasm_stubs.cpp b/tests/gal-regression/wasm/wasm_stubs.cpp new file mode 100644 index 0000000..5c540f3 --- /dev/null +++ b/tests/gal-regression/wasm/wasm_stubs.cpp @@ -0,0 +1,342 @@ +/** + * WASM stubs for GAL WebGL test + * + * Provides implementations for symbols needed by WEBGL_GAL but not + * provided by the minimal test harness. + */ + +// Include kiglew.h first to prevent GLEW conflicts +#include "webgl/kiglew.h" + +#include +#include + +// Forward declarations to avoid including heavy headers +namespace KIGFX { class GAL; } + +// Include minimal headers +#include +#include +#include +#include + +//============================================================================= +// COLOR4D static constants +//============================================================================= + +namespace KIGFX { + +const COLOR4D COLOR4D::BLACK( 0, 0, 0, 1 ); +const COLOR4D COLOR4D::WHITE( 1, 1, 1, 1 ); +const COLOR4D COLOR4D::UNSPECIFIED( 0, 0, 0, 0 ); +const COLOR4D COLOR4D::CLEAR( 0, 0, 0, 0 ); + +} // namespace KIGFX + +//============================================================================= +// KIID +//============================================================================= + +KIID niluuid; + +// Note: GAL class is provided by graphics_abstraction_layer.cpp + +//============================================================================= +// GLU tesselator stubs (needed for polygon rendering) +// These are no-op stubs - polygons won't render correctly until +// we add a real tesselator implementation +//============================================================================= + +extern "C" { + +struct GLUtesselator {}; + +GLUtesselator* gluNewTess() { return new GLUtesselator(); } +void gluDeleteTess(GLUtesselator* tess) { delete tess; } +void gluTessProperty(GLUtesselator*, GLenum, GLdouble) {} +void gluTessCallback(GLUtesselator*, GLenum, void(*)()) {} +void gluTessBeginPolygon(GLUtesselator*, void*) {} +void gluTessEndPolygon(GLUtesselator*) {} +void gluTessBeginContour(GLUtesselator*) {} +void gluTessEndContour(GLUtesselator*) {} +void gluTessVertex(GLUtesselator*, GLdouble*, void*) {} + +} + +//============================================================================= +// Additional stubs from kicad_stubs.cpp that are needed +//============================================================================= + +#include + +namespace UTIL { +namespace DETAIL { + +OBSERVABLE_BASE::OBSERVABLE_BASE() {} +OBSERVABLE_BASE::~OBSERVABLE_BASE() {} +void OBSERVABLE_BASE::on_observers_empty() {} +void OBSERVABLE_BASE::enter_iteration() {} +void OBSERVABLE_BASE::leave_iteration() {} +void OBSERVABLE_BASE::add_observer(void*) {} +void OBSERVABLE_BASE::remove_observer(void*) {} + +} // namespace DETAIL +} // namespace UTIL + +// UTIL::LINK stubs +namespace UTIL { +LINK::LINK() : token_(nullptr), observer_(nullptr) {} +LINK::LINK(std::shared_ptr token, void* observer) + : token_(token), observer_(observer) {} +LINK::LINK(LINK&& other) : token_(std::move(other.token_)), observer_(other.observer_) { + other.observer_ = nullptr; +} +LINK::~LINK() {} +LINK& LINK::operator=(LINK&& other) { + token_ = std::move(other.token_); + observer_ = other.observer_; + other.observer_ = nullptr; + return *this; +} +void LINK::reset() { token_.reset(); observer_ = nullptr; } +LINK::operator bool() const { return token_ != nullptr; } +} + +// UI dialog stubs +void DisplayError(wxWindow*, const wxString&) {} +void DisplayErrorMessage(wxWindow*, const wxString&, const wxString&) {} + +// OpenGL info stub +void SetOpenGLInfo(const char*, const char*, const char*) {} + +// Math logging stub +void kimathLogOverflow(double, const char*) {} + +// Arc geometry stub +#include +int GetArcToSegmentCount(int aRadius, int aArcError, const EDA_ANGLE& aArcAngle) { + double degrees = std::abs(aArcAngle.AsDegrees()); + return std::max(1, (int)(degrees / 10.0)); +} + +// DPI scaling stub +#include +double DPI_SCALING::GetDefaultScaleFactor() { return 1.0; } + +// Cursor store stub +#include +const WX_CURSOR_TYPE CURSOR_STORE::GetCursor(KICURSOR aCursor, bool aHiDPI) { + return wxCursor(wxCURSOR_ARROW); +} + +// VC_SETTINGS::Reset stub +#include +namespace KIGFX { +void VC_SETTINGS::Reset() {} +} + +// KIID constructors +KIID::KIID() {} +KIID::KIID(int aValue) {} + +// UTF8 stubs +#include +UTF8::UTF8(const wxString& s) : m_s(s.ToUTF8().data()) {} +int UTF8::uni_forward(const unsigned char* aSequence, unsigned* aResult) { + if (aSequence && *aSequence) { + if (aResult) *aResult = (unsigned)*aSequence; + return 1; + } + if (aResult) *aResult = 0; + return 0; +} + +// TEXT_ATTRIBUTES constructor +#include +TEXT_ATTRIBUTES::TEXT_ATTRIBUTES(KIFONT::FONT* aFont) {} + +// KIFONT stubs +#include + +namespace KIFONT { + +FONT* FONT::GetFont(const wxString&, bool, bool, const std::vector*, bool) { + return nullptr; +} + +const METRICS& METRICS::Default() { + static METRICS m; + return m; +} + +void OUTLINE_GLYPH::Triangulate(std::function) const {} + +void FONT::Draw(KIGFX::GAL*, const wxString&, const VECTOR2I&, const VECTOR2I&, + const TEXT_ATTRIBUTES&, const METRICS&) const {} + +// STROKE_GLYPH stubs +STROKE_GLYPH::STROKE_GLYPH( const STROKE_GLYPH& aGlyph ) +{ + reserve( aGlyph.size() ); + for( const std::vector& pointList : aGlyph ) + push_back( pointList ); + m_boundingBox = aGlyph.m_boundingBox; + m_penIsDown = false; +} + +void STROKE_GLYPH::AddPoint( const VECTOR2D& aPoint ) +{ + if( !m_penIsDown ) + { + emplace_back(); + back().reserve( 16 ); + m_penIsDown = true; + } + back().push_back( aPoint ); +} + +void STROKE_GLYPH::RaisePen() +{ + m_penIsDown = false; +} + +void STROKE_GLYPH::Finalize() +{ +} + +} // namespace KIFONT + +// SHAPE_POLY_SET stub +#include +bool SHAPE_POLY_SET::IsTriangulationUpToDate() const { + return false; +} + +// Bezier curve stub +#include +void BEZIER_POLY::GetPoly(std::vector& aOutput, double aMinSegLen) { + if (!m_ctrlPts.empty()) { + aOutput.push_back(m_ctrlPts.front()); + aOutput.push_back(m_ctrlPts.back()); + } +} + +// BITMAP_BASE stubs +#include + +BITMAP_BASE::BITMAP_BASE( const VECTOR2I& pos ) +{ + m_scale = 1.0; + m_imageType = wxBITMAP_TYPE_PNG; + m_bitmap = nullptr; + m_image = nullptr; + m_originalImage = nullptr; + m_ppi = 91; + m_pixelSizeIu = 254000.0 / m_ppi; + m_isMirroredX = false; + m_isMirroredY = false; + m_rotation = ANGLE_0; +} + +bool BITMAP_BASE::SetImage( const wxImage& aImage ) +{ + delete m_image; + delete m_originalImage; + delete m_bitmap; + + m_image = new wxImage( aImage ); + m_originalImage = new wxImage( aImage ); + m_bitmap = new wxBitmap( *m_image ); + m_imageType = wxBITMAP_TYPE_PNG; + return true; +} + +//============================================================================= +// PGM_BASE and related stubs (minimal for GL_CONTEXT_MANAGER) +//============================================================================= + +// Forward-declared types +class SETTINGS_MANAGER {}; +class LIBRARY_MANAGER {}; +class BACKGROUND_JOBS_MONITOR {}; +class NOTIFICATIONS_MANAGER {}; + +#include +#include +#include + +// Minimal PGM_BASE subclass +class PGM_BASE_TEST : public PGM_BASE { +public: + PGM_BASE_TEST() : PGM_BASE() {} + void MacOpenFile(const wxString& aFileName) override {} +}; + +static PGM_BASE_TEST* s_pgmInstance = nullptr; + +PGM_BASE& Pgm() { + if (!s_pgmInstance) { + s_pgmInstance = new PGM_BASE_TEST(); + } + return *s_pgmInstance; +} + +const ADVANCED_CFG& ADVANCED_CFG::GetCfg() { + static ADVANCED_CFG instance; + return instance; +} + +ADVANCED_CFG::ADVANCED_CFG() { + m_ScreenDPI = 91; +} + +const char* GetBuildVersion() { + return "8.0.0-wasm-test"; +} + +wxApp& PGM_BASE::App() { + return *wxTheApp; +} + +COMMON_SETTINGS* PGM_BASE::GetCommonSettings() const { + return nullptr; +} + +const wxString& PGM_BASE::GetExecutablePath() const { + static wxString empty; + return empty; +} + +ENV_VAR_MAP& PGM_BASE::GetLocalEnvVariables() const { + static ENV_VAR_MAP empty; + return empty; +} + +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(); } + +PGM_BASE::PGM_BASE() { + m_singleton.Init(); +} +PGM_BASE::~PGM_BASE() {} + +// Singleton init +#include +#include "webgl/gl_context_mgr.h" + +void KICAD_SINGLETON::Init() { + m_GLContextManager = new GL_CONTEXT_MANAGER(); +} +KICAD_SINGLETON::~KICAD_SINGLETON() { + delete m_GLContextManager; + m_GLContextManager = nullptr; +} diff --git a/tests/gal-regression/wasm/webgl/kiglew.h b/tests/gal-regression/wasm/webgl/kiglew.h index 7d049de..44d16b4 100644 --- a/tests/gal-regression/wasm/webgl/kiglew.h +++ b/tests/gal-regression/wasm/webgl/kiglew.h @@ -32,6 +32,12 @@ #include #if defined( __EMSCRIPTEN__ ) + // Prevent real GLEW header from being included (Emscripten has one too) + // We provide our own compatibility stubs below + #ifndef __glew_h__ + #define __glew_h__ + #endif + // WebGL2/GLES3: Modern shader functions (glUseProgram, etc.) #include // Legacy GL emulation: glMatrixMode, glColor4d, glBegin/glEnd, etc.