pcbjam/tests/3d-regression/native/fbo_capture.cpp
Istvan Matejcsok ce1473c9ab 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>
2026-07-03 15:45:58 +02:00

146 lines
5.2 KiB
C++

#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;
}