diff --git a/build-pc.sh b/build-pc.sh old mode 100644 new mode 100755 diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index 2341ece2..19f11a07 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -38,6 +38,10 @@ struct ShaderProgram { GLint frame_count_location; GLint noise_scale_location; GLint three_point_filter_locations[2]; + GLint decal_mode_location; + GLint decal_scale_location; + int decal_mode; + float decal_scale; }; struct Framebuffer { @@ -45,6 +49,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; }; @@ -63,6 +69,19 @@ static int current_anisotropy_level = 4; /* B1: sane default (0 was invalid fo 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; + +// N64 decal depth mode: pixels only pass when close to the stored depth, using a copy of the depth buffer +#define DECAL_DEPTH_TEXTURE_UNIT 2 +// Height of the N64 frame; the RDP's depth slope tolerance is measured in N64 pixels +#define DECAL_N64_SCREEN_HEIGHT 240.0f +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 int gl_glsl_version = 130; static char gl_glsl_version_str[16] = "130"; @@ -127,6 +146,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); @@ -396,6 +416,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 float uDecalPixelScale;"); + 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));"); @@ -455,6 +478,16 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad } append_line(fs_buf, &fs_len, "void main() {"); + + // RDP decal Z mode: pass only if |z - stored z| <= dz (the per-pixel depth slope), and never over cleared depth. + // The slope is computed before anything can discard, since derivatives are undefined after a neighbor discards. + append_line(fs_buf, &fs_len, " highp float decalSlope = fwidth(gl_FragCoord.z);"); + append_line(fs_buf, &fs_len, " if (uDecalMode != 0) {"); + append_line(fs_buf, &fs_len, " highp float storedZ = texelFetch(uDepthTex, ivec2(gl_FragCoord.xy), 0).r;"); + append_line(fs_buf, &fs_len, " highp float dz = max(decalSlope * uDecalPixelScale, 1.0 / 262144.0);"); + append_line(fs_buf, &fs_len, " if (storedZ >= 1.0 || abs(gl_FragCoord.z - storedZ) > dz) discard;"); + append_line(fs_buf, &fs_len, " }"); + /* D548: combiner inputs are 8-bit on the N64 (shade, prim, env), so never * above 1.0. Under the D540 noperspective shade, GPU clipping of a big * near-camera triangle can extrapolate them past 1.0; the CC wrap @@ -703,6 +736,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_scale_location = glGetUniformLocation(shader_program, "uDecalPixelScale"); + prg->decal_mode = 0; + prg->decal_scale = 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); @@ -722,6 +763,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); } @@ -830,9 +872,9 @@ static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint } mipmaps = mipmaps && (current_mipmap_filter_mode != MIPMAP_DISABLED); const int mip_idx = mipmaps ? current_mipmap_filter_mode : 0; - const GLint min_filter = linear_filter ? min_filters[current_filter_mode][mip_idx] : GL_NEAREST; - /* Trilinear option (playtest 2026-10-03): TRILINEAR magnifies LINEAR too. */ - const GLint max_filter = linear_filter && (current_filter_mode == FILTER_LINEAR || current_filter_mode == FILTER_TRILINEAR) ? GL_LINEAR : GL_NEAREST; + const GLint min_filter = linear_filter ? min_filters[current_filter_mode][mip_idx] : GL_LINEAR; + // Point-sampled textures (e.g. HUD ammo icons) are filtered too + const GLint max_filter = GL_LINEAR; glActiveTexture(GL_TEXTURE0 + tile); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); @@ -859,6 +901,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); @@ -928,10 +973,122 @@ static void gfx_opengl_set_use_alpha(bool use_alpha, bool modulate) { } } +static GLuint gfx_opengl_framebuffer_name(size_t fb_id) { + // fb 0 is the window's default framebuffer (GL name 0) + return fb_id == 0 ? 0 : framebuffers[fb_id].fbo; +} + +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); + glBindFramebuffer(GL_FRAMEBUFFER, gfx_opengl_framebuffer_name(current_framebuffer)); + + 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, gfx_opengl_framebuffer_name(current_framebuffer)); + 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, gfx_opengl_framebuffer_name(current_framebuffer)); + 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; + } + + const float scale = framebuffers[current_framebuffer].height / DECAL_N64_SCREEN_HEIGHT; + if (enabled && prg->decal_scale != scale) { + glUniform1f(prg->decal_scale_location, scale); + prg->decal_scale = scale; + } +} + 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); + if (current_depth_decal) { + // The shader replaces the depth test like the RDP does; fall back to polygon offset if depth can't be copied + const bool decal = gfx_opengl_update_depth_copy(); + glDepthFunc(decal ? GL_ALWAYS : GL_LEQUAL); + if (decal) { + glDisable(GL_POLYGON_OFFSET_FILL); + } else { + glEnable(GL_POLYGON_OFFSET_FILL); + } + gfx_opengl_set_decal_uniforms(current_shader_program, decal); + } else { + gfx_opengl_set_decal_uniforms(current_shader_program, false); + } + 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, @@ -1122,6 +1279,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; @@ -1455,6 +1617,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) { @@ -1469,6 +1632,7 @@ bool gfx_opengl_start_draw_to_framebuffer(int fb_id, float noise_scale) { * GL implementation like Mesa/llvmpipe). */ glBindFramebuffer(GL_FRAMEBUFFER, fb_id == 0 ? 0 : fb.fbo); current_framebuffer = fb_id; + depth_copy_dirty = true; return true; } else { return false; @@ -1486,6 +1650,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) { @@ -1509,6 +1674,7 @@ void gfx_opengl_clear_region(bool clear_color, bool clear_depth, int x, int y, i if (clear_depth) { glDepthMask(GL_TRUE); mask |= GL_DEPTH_BUFFER_BIT; + depth_copy_dirty = true; } glScissor(x, y, w, h); glEnable(GL_SCISSOR_TEST);