From 60989596d5768cc5d5cb9e4d17fb7e754fac97c3 Mon Sep 17 00:00:00 2001 From: Maxime Morel Date: Wed, 21 Oct 2026 19:31:03 +0200 Subject: [PATCH] Decals fix --- port/fast3d/gfx_opengl.cpp | 158 +++++++++++++++++++++++++++++++- port/fast3d/gfx_pc.cpp | 35 +++++++ port/fast3d/gfx_rendering_api.h | 1 + 3 files changed, 193 insertions(+), 1 deletion(-) diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index 76427900f..327e3ac83 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -33,6 +33,10 @@ struct ShaderProgram { GLint frame_count_location; GLint noise_scale_location; GLint three_point_filter_locations[2]; + GLint decal_mode_location; + GLint decal_depth_location; + int decal_mode; + float decal_depth[2]; }; struct Framebuffer { @@ -40,6 +44,8 @@ struct Framebuffer { bool has_depth_buffer; uint32_t msaa_level; bool invert_y; + bool depth_copy_checked; + bool depth_copy_failed; GLuint fbo, clrbuf, clrbuf_msaa, rbo; }; @@ -58,6 +64,19 @@ static int current_anisotropy_level; static FilteringMode current_filter_mode = FILTER_LINEAR; static MipmapFilteringMode current_mipmap_filter_mode = MIPMAP_LINEAR; static bool current_textures_linear_filter[2] = {false, false}; +static struct ShaderProgram* current_shader_program; + +// Decal depth proximity: decal pixels are discarded if the surface below them is much further away +#define DECAL_DEPTH_TEXTURE_UNIT 2 +static bool depth_copy_supported; +static bool depth_copy_dirty = true; +static size_t depth_copy_source; +static GLuint depth_copy_fbo, depth_copy_tex; +static uint32_t depth_copy_width, depth_copy_height; +static bool current_depth_test; +static bool current_depth_decal; +static bool decal_projection_valid; +static float decal_depth_params[2]; static int gl_glsl_version = 130; static char gl_glsl_version_str[16] = "130"; @@ -120,6 +139,7 @@ static void gfx_opengl_unload_shader(struct ShaderProgram* old_prg) { static void gfx_opengl_load_shader(struct ShaderProgram* new_prg) { // if (!new_prg) return; + current_shader_program = new_prg; glUseProgram(new_prg->opengl_program_id); gfx_opengl_vertex_array_set_attribs(new_prg); gfx_opengl_set_uniforms(new_prg); @@ -382,6 +402,9 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad append_line(fs_buf, &fs_len, "uniform int frame_count;"); append_line(fs_buf, &fs_len, "uniform float noise_scale;"); + append_line(fs_buf, &fs_len, "uniform int uDecalMode;"); + append_line(fs_buf, &fs_len, "uniform highp vec2 uDecalDepth;"); + append_line(fs_buf, &fs_len, "uniform highp sampler2D uDepthTex;"); append_line(fs_buf, &fs_len, "float random(in vec3 value) {"); append_line(fs_buf, &fs_len, " float random = dot(sin(value), vec3(12.9898, 78.233, 37.719));"); @@ -442,6 +465,15 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad append_line(fs_buf, &fs_len, "void main() {"); + // Window depth is c0 + c1 / w, so the stored depth can be converted back to the clip-space W of the surface. + // Reject decal pixels that hang past an edge over a surface that is further away than a small tolerance. + append_line(fs_buf, &fs_len, " if (uDecalMode != 0) {"); + append_line(fs_buf, &fs_len, " highp float fragW = 1.0 / gl_FragCoord.w;"); + append_line(fs_buf, &fs_len, " highp float surfaceZ = texelFetch(uDepthTex, ivec2(gl_FragCoord.xy), 0).r;"); + append_line(fs_buf, &fs_len, " highp float surfaceW = uDecalDepth.y / (surfaceZ - uDecalDepth.x);"); + append_line(fs_buf, &fs_len, " if (surfaceW - fragW > fragW * 0.03 + 2.0 * fwidth(fragW)) discard;"); + append_line(fs_buf, &fs_len, " }"); + for (int i = 0; i < 2; i++) { if (cc_features.used_textures[i]) { bool s = cc_features.clamp[i][0], t = cc_features.clamp[i][1]; @@ -652,6 +684,14 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad prg->noise_scale_location = glGetUniformLocation(shader_program, "noise_scale"); prg->three_point_filter_locations[0] = glGetUniformLocation(shader_program, "three_point_filter0"); prg->three_point_filter_locations[1] = glGetUniformLocation(shader_program, "three_point_filter1"); + prg->decal_mode_location = glGetUniformLocation(shader_program, "uDecalMode"); + prg->decal_depth_location = glGetUniformLocation(shader_program, "uDecalDepth"); + prg->decal_mode = 0; + prg->decal_depth[0] = prg->decal_depth[1] = 0.0f; + if (prg->decal_mode_location >= 0) { + glUniform1i(prg->decal_mode_location, 0); + glUniform1i(glGetUniformLocation(shader_program, "uDepthTex"), DECAL_DEPTH_TEXTURE_UNIT); + } gfx_opengl_load_shader(prg); @@ -671,6 +711,7 @@ static void gfx_opengl_shader_get_info(struct ShaderProgram* prg, uint8_t* num_i static void gfx_opengl_clear_shaders(void) { glUseProgram(0); + current_shader_program = nullptr; for (auto& pair : shader_program_pool) { glDeleteProgram(pair.second.opengl_program_id); } @@ -741,6 +782,9 @@ static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint } static void gfx_opengl_set_depth_mode(bool depth_test, bool depth_update, bool depth_compare, bool depth_source_prim, uint16_t zmode) { + current_depth_test = depth_test; + current_depth_decal = depth_test && depth_compare && zmode == ZMODE_DEC; + if (depth_test) { glEnable(GL_DEPTH_TEST); glDepthMask(depth_update ? GL_TRUE : GL_FALSE); @@ -810,10 +854,113 @@ static void gfx_opengl_set_use_alpha(bool use_alpha, bool modulate) { } } +static void gfx_opengl_set_decal_projection(bool valid, float alpha, float beta) { + // Clip-space Z is alpha * W + beta; convert that to window depth, including the no-depth-clamp Z scale hack + const float zscale = GLAD_GL_ARB_depth_clamp ? 1.0f : 0.3f; + decal_projection_valid = valid; + decal_depth_params[0] = 0.5f + 0.5f * zscale * alpha; + decal_depth_params[1] = 0.5f * zscale * beta; +} + +static bool gfx_opengl_update_depth_copy(void) { + if (!depth_copy_supported) { + return false; + } + + Framebuffer& fb = framebuffers[current_framebuffer]; + if ((current_framebuffer != 0 && !fb.has_depth_buffer) || fb.depth_copy_failed) { + return false; + } + if (!depth_copy_dirty && depth_copy_source == current_framebuffer && depth_copy_width == fb.width && + depth_copy_height == fb.height) { + return true; + } + + if (depth_copy_tex == 0 || depth_copy_width != fb.width || depth_copy_height != fb.height) { + if (depth_copy_tex == 0) { + glGenTextures(1, &depth_copy_tex); + glGenFramebuffers(1, &depth_copy_fbo); + } + + GLint active_texture; + glGetIntegerv(GL_ACTIVE_TEXTURE, &active_texture); + glActiveTexture(GL_TEXTURE0 + DECAL_DEPTH_TEXTURE_UNIT); + glBindTexture(GL_TEXTURE_2D, depth_copy_tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, fb.width, fb.height, 0, GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glActiveTexture(active_texture); + + const GLenum no_draw_buffer = GL_NONE; + glBindFramebuffer(GL_FRAMEBUFFER, depth_copy_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depth_copy_tex, 0); + glDrawBuffers(1, &no_draw_buffer); + glReadBuffer(GL_NONE); + + depth_copy_width = fb.width; + depth_copy_height = fb.height; + } + + const bool check_errors = !fb.depth_copy_checked; + if (check_errors) { + for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; i++) { + } + } + + glDisable(GL_SCISSOR_TEST); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fb.fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, depth_copy_fbo); + glBlitFramebuffer(0, 0, fb.width, fb.height, 0, 0, fb.width, fb.height, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo); + glEnable(GL_SCISSOR_TEST); + + if (check_errors) { + fb.depth_copy_checked = true; + if (glGetError() != GL_NO_ERROR) { + fb.depth_copy_failed = true; + sysLogPrintf(LOG_WARNING, "GL: could not copy depth buffer of framebuffer %u, decal edges will not be clipped", + (unsigned)current_framebuffer); + return false; + } + } + + depth_copy_dirty = false; + depth_copy_source = current_framebuffer; + return true; +} + +static void gfx_opengl_set_decal_uniforms(struct ShaderProgram* prg, bool enabled) { + if (prg == nullptr || prg->decal_mode_location < 0) { + return; + } + + const int mode = enabled ? 1 : 0; + if (prg->decal_mode != mode) { + glUniform1i(prg->decal_mode_location, mode); + prg->decal_mode = mode; + } + if (enabled && + (prg->decal_depth[0] != decal_depth_params[0] || prg->decal_depth[1] != decal_depth_params[1])) { + glUniform2f(prg->decal_depth_location, decal_depth_params[0], decal_depth_params[1]); + prg->decal_depth[0] = decal_depth_params[0]; + prg->decal_depth[1] = decal_depth_params[1]; + } +} + static void gfx_opengl_draw_triangles(float buf_vbo[], size_t buf_vbo_len, size_t buf_vbo_num_tris) { // printf("flushing %d tris\n", buf_vbo_num_tris); + const bool decal = current_depth_decal && decal_projection_valid && gfx_opengl_update_depth_copy(); + gfx_opengl_set_decal_uniforms(current_shader_program, decal); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, buf_vbo, GL_STREAM_DRAW); glDrawArrays(GL_TRIANGLES, 0, 3 * buf_vbo_num_tris); + + if (current_depth_test && current_depth_mask) { + depth_copy_dirty = true; + } } typedef void (APIENTRY *DEBUGPROC)(GLenum source, @@ -990,6 +1137,11 @@ static void gfx_opengl_init(void) { gfx_framebuffers_enabled = false; } + depth_copy_supported = GLVersion.major >= 3 && gfx_opengl_supports_framebuffers() && glDrawBuffers && glReadBuffer; + if (!depth_copy_supported) { + sysLogPrintf(LOG_WARNING, "GL: depth buffer copies unsupported, decal edges will not be clipped"); + } + if ((GLVersion.major < 4 || GLVersion.minor < 4) && !GLAD_GL_ARB_texture_mirror_clamp_to_edge) { // GL_MIRROR_CLAMP_TO_EDGE unsupported gl_mirror_clamp = GL_MIRRORED_REPEAT; @@ -1138,6 +1290,7 @@ static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width, fb.has_depth_buffer = has_depth_buffer; fb.msaa_level = msaa_level; fb.invert_y = opengl_invert_y; + depth_copy_dirty = true; } bool gfx_opengl_start_draw_to_framebuffer(int fb_id, float noise_scale) { @@ -1148,6 +1301,7 @@ bool gfx_opengl_start_draw_to_framebuffer(int fb_id, float noise_scale) { } glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo); current_framebuffer = fb_id; + depth_copy_dirty = true; return true; } else { return false; @@ -1165,6 +1319,7 @@ void gfx_opengl_clear_framebuffer(bool clear_color, bool clear_depth) { if (clear_depth) { glDepthMask(GL_TRUE); mask |= GL_DEPTH_BUFFER_BIT; + depth_copy_dirty = true; } glClear(mask); if (clear_depth) { @@ -1317,5 +1472,6 @@ struct GfxRenderingAPI gfx_opengl_api = { gfx_opengl_get_texture_filter, gfx_opengl_set_mipmap_filter, gfx_opengl_set_anisotropy_level, - gfx_opengl_get_max_anisotropy_level + gfx_opengl_get_max_anisotropy_level, + gfx_opengl_set_decal_projection }; diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index e0f66734d..25517e682 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -204,6 +204,8 @@ static struct RenderingState { struct XYWidthHeight viewport, scissor; struct ShaderProgram* shader_program; TextureCacheNode* textures[SHADER_MAX_TEXTURES]; + bool decal_projection_valid; + float decal_projection[2]; } rendering_state; struct GfxDimensions gfx_current_window_dimensions; @@ -1274,6 +1276,39 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo rendering_state.depth_mode = depth_mode; } + if (depth_test && depth_compare && zmode == ZMODE_DEC) { + // For perspective projections the clip-space Z column is alpha * W column + beta, + // which lets the renderer turn stored depth back into distance for decal proximity tests + int k = 0; + for (int i = 1; i < 3; i++) { + if (fabsf(rsp.P_matrix[i][3]) > fabsf(rsp.P_matrix[k][3])) { + k = i; + } + } + + bool valid = fabsf(rsp.P_matrix[k][3]) > 1e-6f; + float alpha = 0.0f, beta = 0.0f; + if (valid) { + alpha = rsp.P_matrix[k][2] / rsp.P_matrix[k][3]; + beta = rsp.P_matrix[3][2] - alpha * rsp.P_matrix[3][3]; + for (int i = 0; i < 3; i++) { + if (fabsf(rsp.P_matrix[i][2] - alpha * rsp.P_matrix[i][3]) > 1e-4f * (1.0f + fabsf(rsp.P_matrix[i][2]))) { + valid = false; + } + } + valid = valid && fabsf(beta) > 1e-6f; + } + + if (valid != rendering_state.decal_projection_valid || alpha != rendering_state.decal_projection[0] || + beta != rendering_state.decal_projection[1]) { + gfx_flush(); + gfx_rapi->set_decal_projection(valid, alpha, beta); + rendering_state.decal_projection_valid = valid; + rendering_state.decal_projection[0] = alpha; + rendering_state.decal_projection[1] = beta; + } + } + if (rdp.viewport_or_scissor_changed) { if (memcmp(&rdp.viewport, &rendering_state.viewport, sizeof(rdp.viewport)) != 0) { gfx_flush(); diff --git a/port/fast3d/gfx_rendering_api.h b/port/fast3d/gfx_rendering_api.h index d21b3f759..93d7005bf 100644 --- a/port/fast3d/gfx_rendering_api.h +++ b/port/fast3d/gfx_rendering_api.h @@ -56,6 +56,7 @@ struct GfxRenderingAPI { void (*set_mipmap_filter)(enum MipmapFilteringMode mode); void (*set_anisotropy_level)(int); int (*get_max_anisotropy_level)(void); + void (*set_decal_projection)(bool valid, float alpha, float beta); }; #endif