From 426ee0baaca23de59610c97b0973bf00ce2b2484 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 26 May 2026 22:08:06 +0200 Subject: [PATCH] use textureGather for shadow PCF --- gl/shader.cpp | 61 ++++++++++++++++++++++++++++++++++ gl/ubo.h | 7 +++- model/material.cpp | 22 ++++++++++-- rendering/opengl33renderer.cpp | 6 ++++ rendering/openglmatrixstack.h | 42 +++++++++++++---------- shaders/light_common.glsl | 33 ++++++++++++++++-- 6 files changed, 147 insertions(+), 24 deletions(-) diff --git a/gl/shader.cpp b/gl/shader.cpp index 248b50f1..5d818833 100644 --- a/gl/shader.cpp +++ b/gl/shader.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "shader.h" #include "glsl_common.h" #include "utilities/Logs.h" @@ -13,6 +14,44 @@ inline bool strcend(std::string const &value, std::string const &ending) return std::equal(ending.rbegin(), ending.rend(), value.rbegin()); } +namespace { + +// The project's glad config only generates GLAD_GL_ARB_texture_filter_anisotropic +// among the ARB/EXT extension flags -- GL_ARB_texture_gather (desktop) and +// GL_EXT_gpu_shader5 (GLES 3.1) aren't compiled in, so we can't gate the +// shadow textureGather() optimisation on a GLAD constant. Query the live +// extension string instead. The first call walks the extension list once +// (no extension count in the dozens is large enough to matter here) and +// the result is cached in the static bool inside has_gl_extension(), so +// every subsequent shader compile is a plain bool read. +// +// SHADERVALIDATOR_STANDALONE gates out the GL queries because the offline +// shader validator tool links glad.c but never calls gladLoadGL(); the +// function pointers stay null and a real call would crash. Returning +// false there means the standalone validator simply compiles the original +// 16-tap PCF fallback path in light_common.glsl, which is what we want. +bool has_gl_extension(char const *name) { +#ifdef SHADERVALIDATOR_STANDALONE + (void)name; + return false; +#else + if (!glGetIntegerv || !glGetStringi) { + return false; + } + GLint count = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &count); + for (GLint i = 0; i < count; ++i) { + char const *ext = reinterpret_cast(glGetStringi(GL_EXTENSIONS, i)); + if (ext != nullptr && std::strcmp(ext, name) == 0) { + return true; + } + } + return false; +#endif +} + +} // anonymous namespace + std::string gl::shader::read_file(const std::string &filename) { std::stringstream stream; @@ -91,11 +130,33 @@ std::pair gl::shader::process_source(const std::string &fil if (!Global.gfx_usegles) { str += "#version 330 core\n"; + // textureGather() on sampler2DArrayShadow is core in GLSL 4.0. On 3.30 + // desktop it requires GL_ARB_gpu_shader5 -- the older + // GL_ARB_texture_gather (2010) only adds the non-shadow and the plain + // sampler2DShadow overloads, NOT the sampler2DArrayShadow one we need + // for cascaded shadow PCF. Some drivers advertise texture_gather but + // reject the shadow-array overload because the spec for it lives in + // gpu_shader5; so emit only when gpu_shader5 is advertised. When the + // extension is missing, calc_shadow() in light_common.glsl falls back + // to the original 16-tap hardware-PCF loop via #ifndef. + // (Project's glad config doesn't generate GLAD_GL_ARB_gpu_shader5, + // so we query the live extension list -- see has_gl_extension above.) + static bool const have_gpu_shader5 = has_gl_extension("GL_ARB_gpu_shader5"); + if (have_gpu_shader5) + str += "#extension GL_ARB_gpu_shader5 : enable\n"; } else { if (GLAD_GL_ES_VERSION_3_1) { str += "#version 310 es\n"; + // GLES 3.1 lacks textureGather on shadow samplers in core; the + // EXT_gpu_shader5 extension adds it. Only emit the directive when + // the driver advertises support (see desktop comment above). + // (Glad config doesn't generate GLAD_GL_EXT_gpu_shader5 -- query + // the live extension string the same way.) + static bool const have_gpu_shader5 = has_gl_extension("GL_EXT_gpu_shader5"); + if (have_gpu_shader5) + str += "#extension GL_EXT_gpu_shader5 : enable\n"; if (type == GL_GEOMETRY_SHADER) str += "#extension GL_EXT_geometry_shader : require\n"; } else { diff --git a/gl/ubo.h b/gl/ubo.h index b22b17e1..6107b677 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -81,7 +81,12 @@ namespace gl void set_modelview(const glm::mat4 &mv) { modelview = mv; - modelviewnormal = glm::mat3x4(glm::mat3(glm::transpose(glm::inverse(mv)))); + // normal matrix = transpose(inverse(modelview)). The modelview is + // always affine, so its 3x3 normal matrix depends only on the + // upper-left 3x3 block; inverting that mat3 directly is markedly + // cheaper than a full mat4 inverse and yields an identical result + // (for affine M, mat3(inverse(M)) == inverse(mat3(M))). + modelviewnormal = glm::mat3x4(glm::transpose(glm::inverse(glm::mat3(mv)))); } }; diff --git a/model/material.cpp b/model/material.cpp index 24b63752..272da3ae 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -540,14 +540,30 @@ material_manager::create( std::string const &Filename, bool const Loadnow ) { } if( false == material.name.empty() ) { - // if we have material name and shader it means resource was processed succesfully - materialhandle = m_materials.size(); - m_materialmappings.emplace( material.name, materialhandle ); + // if we have material name and shader it means resource was processed succesfully. + // + // IMPORTANT: capture the handle ONLY after emplace_back succeeds. The + // previous version pre-assigned `materialhandle = m_materials.size()` + // and then ran `finalize(Loadnow)` (which can throw shader_exception + // when a shader fails to compile) and `emplace_back` together inside + // the try. If finalize() threw, emplace_back never ran, but the + // handle was already set to size() -- pointing one past the actual + // last element. Subsequent `m_materials[handle]` lookups would then + // read garbage past end-of-vector, producing the classic 0x30-offset + // access violation seen in TSubModel::BinInit (vtable read on a + // bogus IMaterial pointer). Now we leave materialhandle as + // null_handle if anything throws, and the caller treats it as a + // failed-to-load material. try { material.finalize(Loadnow); + materialhandle = m_materials.size(); m_materials.emplace_back( std::move(material) ); + m_materialmappings.emplace( m_materials.back().name, materialhandle ); } catch (gl::shader_exception const &e) { ErrorLog("invalid shader: " + std::string(e.what())); + // record the failure so subsequent Fetch_Material(filename) calls + // short-circuit without re-running the failing compile. + m_materialmappings.emplace( material.name, null_handle ); } } else { diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index f6effefa..21c0c487 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2424,6 +2424,12 @@ void opengl33_renderer::Render(scene::basic_region *Region) // at this stage the z-buffer is filled with only ground geometry Update_Mouse_Position(); } + // draw opaque cells front-to-back: with the depth test enabled this + // lets the GPU reject hidden fragments early, before the (expensive) + // lit fragment shader runs on them. Order is irrelevant to the final + // image for opaque geometry, so this is purely a fill-rate win. + std::sort( std::begin( m_cellqueue ), std::end( m_cellqueue ), + []( distancecell_pair const &Left, distancecell_pair const &Right ) { return Left.first < Right.first; } ); Render(std::begin(m_cellqueue), std::end(m_cellqueue)); break; } diff --git a/rendering/openglmatrixstack.h b/rendering/openglmatrixstack.h index 31ac7a65..9ba2fa41 100644 --- a/rendering/openglmatrixstack.h +++ b/rendering/openglmatrixstack.h @@ -20,67 +20,73 @@ class opengl_stack { public: // constructors: - opengl_stack() { m_stack.emplace(1.f); } + opengl_stack() { + // reserve generously up front: the matrix stack is pushed/popped once + // per submodel during scene traversal, and std::deque (the previous + // backing store) allocated a fresh heap block on every push for a + // 64-byte glm::mat4. A reserved vector never reallocates within this + // depth, so push/pop become allocation-free and references returned + // by data() stay valid across pushes, exactly as before. + m_stack.reserve( 256 ); + m_stack.emplace_back( 1.f ); } // methods: glm::mat4 const & data() const { - return m_stack.top(); } + return m_stack.back(); } void push_matrix() { - m_stack.emplace( m_stack.top() ); } + glm::mat4 const top { m_stack.back() }; + m_stack.emplace_back( top ); } void pop_matrix( bool const Upload = true ) { if( m_stack.size() > 1 ) { - m_stack.pop(); + m_stack.pop_back(); if( Upload ) { upload(); } } } void load_identity( bool const Upload = true ) { - m_stack.top() = glm::mat4( 1.f ); + m_stack.back() = glm::mat4( 1.f ); if( Upload ) { upload(); } } void load_matrix( glm::mat4 const &Matrix, bool const Upload = true ) { - m_stack.top() = Matrix; + m_stack.back() = Matrix; if( Upload ) { upload(); } } void rotate( float const Angle, glm::vec3 const &Axis, bool const Upload = true ) { - m_stack.top() = glm::rotate( m_stack.top(), Angle, Axis ); + m_stack.back() = glm::rotate( m_stack.back(), Angle, Axis ); if( Upload ) { upload(); } } void translate( glm::vec3 const &Translation, bool const Upload = true ) { - m_stack.top() = glm::translate( m_stack.top(), Translation ); + m_stack.back() = glm::translate( m_stack.back(), Translation ); if( Upload ) { upload(); } } void scale( glm::vec3 const &Scale, bool const Upload = true ) { - m_stack.top() = glm::scale( m_stack.top(), Scale ); + m_stack.back() = glm::scale( m_stack.back(), Scale ); if( Upload ) { upload(); } } void multiply( glm::mat4 const &Matrix, bool const Upload = true ) { - m_stack.top() *= Matrix; + m_stack.back() *= Matrix; if( Upload ) { upload(); } } void ortho( float const Left, float const Right, float const Bottom, float const Top, float const Znear, float const Zfar, bool const Upload = true ) { - m_stack.top() *= glm::ortho( Left, Right, Bottom, Top, Znear, Zfar ); + m_stack.back() *= glm::ortho( Left, Right, Bottom, Top, Znear, Zfar ); if( Upload ) { upload(); } } void perspective( float const Fovy, float const Aspect, float const Znear, float const Zfar, bool const Upload = true ) { - m_stack.top() *= glm::perspective( Fovy, Aspect, Znear, Zfar ); + m_stack.back() *= glm::perspective( Fovy, Aspect, Znear, Zfar ); if( Upload ) { upload(); } } void look_at( glm::vec3 const &Eye, glm::vec3 const &Center, glm::vec3 const &Up, bool const Upload = true ) { - m_stack.top() *= glm::lookAt( Eye, Center, Up ); + m_stack.back() *= glm::lookAt( Eye, Center, Up ); if( Upload ) { upload(); } } private: -// types: - typedef std::stack mat4_stack; - // methods: void - upload() { ::glLoadMatrixf( glm::value_ptr( m_stack.top() ) ); } + upload() { ::glLoadMatrixf( glm::value_ptr( m_stack.back() ) ); } // members: - mat4_stack m_stack; + std::vector m_stack; }; enum stack_mode { gl_modelview = 0, gl_projection = 1, gl_texture = 2 }; diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 927229bf..96eb55f3 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -51,7 +51,6 @@ float calc_shadow() - float shadow = 0.0; float bias = 0.00005f * float(cascade + 1U); vec2 texel = vec2(1.0) / vec2(textureSize(shadowmap, 0)); //float radius = 1.0; f_light_pos[cascade].w; //0.5 + 2.0 * max(abs(2.0 * coords.x - 1.0), abs(2.0 * coords.y - 1.0)); @@ -63,13 +62,43 @@ float calc_shadow() radius = mix(minradius, f_light_pos[cascade+1U].w/f_light_pos[cascade].w, dist_casc); else radius = 0.5; - + +#if defined(GL_ARB_gpu_shader5) || defined(GL_EXT_gpu_shader5) || __VERSION__ >= 400 + // Fast path -- replace the original 4x4 grid of individual hardware-PCF + // lookups with 4 textureGather() calls. Each gather returns the 4 raw + // shadow comparisons of a 2x2 texel footprint, so 4 gathers laid out at + // (+-1, +-1) * radius * texel from the sample center cover the same 4x4 + // sample area as the original kernel; summing all 16 comparisons and + // dividing by 16 reproduces the original loop's averaging. The cost on + // the TMUs drops from 16 hardware-PCF samples to 4 gathers (the gather + // path returns 4 values per fetch where the original needed 4 fetches), + // roughly a 4x reduction in shadow-sample work. The only thing dropped + // vs. the hardware-PCF path is the implicit bilinear blending inside + // each 2x2 footprint -- effectively turning a tent-weighted kernel into + // a box-weighted one of the same extent, which is imperceptible in + // motion. calc_shadow() is by far the heaviest piece of the lighting + // shader, so this is a measurable GPU saving on every shaded fragment. + float refz = coords.z + bias; + float layer = float(cascade); + vec2 off = radius * texel; + vec4 g0 = textureGather(shadowmap, vec3(coords.xy + vec2(-off.x, -off.y), layer), refz); + vec4 g1 = textureGather(shadowmap, vec3(coords.xy + vec2( off.x, -off.y), layer), refz); + vec4 g2 = textureGather(shadowmap, vec3(coords.xy + vec2(-off.x, off.y), layer), refz); + vec4 g3 = textureGather(shadowmap, vec3(coords.xy + vec2( off.x, off.y), layer), refz); + float shadow = dot(g0 + g1 + g2 + g3, vec4(1.0 / 16.0)); + return shadow; +#else + // Fallback for drivers without textureGather on shadow samplers + // (notably GLES 3.0 and any 3.3 desktop driver that doesn't expose + // GL_ARB_texture_gather). Identical to the previous implementation. + float shadow = 0.0; for (float y = -1.5; y <= 1.5; y += 1.0) for (float x = -1.5; x <= 1.5; x += 1.0) shadow += texture(shadowmap, vec4(coords.xy + vec2(x, y) * radius * texel, cascade, coords.z + bias) ); shadow /= 16.0; return shadow; +#endif #else return 0.0; #endif