From 1602b05a2ee58db897a924b58501456299e73ae1 Mon Sep 17 00:00:00 2001 From: water111 <48171810+water111@users.noreply.github.com> Date: Sun, 17 Jul 2022 14:45:00 -0400 Subject: [PATCH] [graphics] some blit optimizations (#1667) * [graphics] a few optimizations * fullscreen crap * working, other than aspect ratio thing * same behavior as before * fix blackout bug, add more error messages * fix error when 0 size buffer * rm warning * one last 0 size issue --- game/graphics/display.h | 4 +- .../graphics/opengl_renderer/BucketRenderer.h | 54 +-- .../opengl_renderer/DirectRenderer.cpp | 5 +- .../opengl_renderer/OpenGLRenderer.cpp | 387 ++++++++++++------ .../graphics/opengl_renderer/OpenGLRenderer.h | 77 +++- game/graphics/opengl_renderer/Sprite3.cpp | 32 +- .../graphics/opengl_renderer/opengl_utils.cpp | 2 +- game/graphics/pipelines/opengl.cpp | 97 +++-- 8 files changed, 424 insertions(+), 234 deletions(-) diff --git a/game/graphics/display.h b/game/graphics/display.h index 04c5edd74..1e3091660 100644 --- a/game/graphics/display.h +++ b/game/graphics/display.h @@ -26,14 +26,14 @@ class GfxDisplay { int m_xpos; int m_ypos; - GfxDisplayMode m_fullscreen_target_mode = GfxDisplayMode::Windowed; - GfxDisplayMode m_last_fullscreen_mode; int m_fullscreen_screen; int m_fullscreen_target_screen; bool m_imgui_visible; protected: bool m_main; + GfxDisplayMode m_fullscreen_target_mode = GfxDisplayMode::Windowed; + GfxDisplayMode m_last_fullscreen_mode; public: virtual ~GfxDisplay() {} diff --git a/game/graphics/opengl_renderer/BucketRenderer.h b/game/graphics/opengl_renderer/BucketRenderer.h index 8c11760c8..cd5f8680d 100644 --- a/game/graphics/opengl_renderer/BucketRenderer.h +++ b/game/graphics/opengl_renderer/BucketRenderer.h @@ -16,41 +16,6 @@ struct LevelVis { u8 data[2048]; }; -struct FboState { - GLuint fbo = -1; - GLuint tex = -1; - GLuint fbo2 = -1; - GLuint tex2 = -1; - GLuint zbuf = -1; - GLenum render_targets[1] = {GL_COLOR_ATTACHMENT0}; - int width = 640; - int height = 480; - int msaa = 4; - - void delete_objects() { - if (fbo != -1) { - glDeleteFramebuffers(1, &fbo); - fbo = -1; - } - if (tex != -1) { - glDeleteTextures(1, &tex); - tex = -1; - } - if (fbo2 != -1) { - glDeleteFramebuffers(1, &fbo2); - fbo2 = -1; - } - if (tex2 != -1) { - glDeleteTextures(1, &tex2); - tex2 = -1; - } - if (zbuf != -1) { - glDeleteRenderbuffers(1, &zbuf); - zbuf = -1; - } - } -}; - class EyeRenderer; /*! * The main renderer will contain a single SharedRenderState that's passed to all bucket renderers. @@ -92,12 +57,21 @@ struct SharedRenderState { std::string load_status_debug; - int window_width_px; - int window_height_px; - int window_offset_x_px; - int window_offset_y_px; + // Information for renderers that need to read framebuffers: + // Most renderers can just use the framebuffer/glViewport set up by OpenGLRenderer, but special + // effects like sprite distort that read the framebuffer will need to know the details of the + // framebuffer setup. - FboState fbo_state; + // the framebuffer that bucket renderers should render to. + int render_fb_w = 0; + int render_fb_h = 0; + GLuint render_fb = -1; + + // the region within that framebuffer to draw to. + int draw_region_w = 0; + int draw_region_h = 0; + int draw_offset_x = 0; + int draw_offset_y = 0; }; /*! diff --git a/game/graphics/opengl_renderer/DirectRenderer.cpp b/game/graphics/opengl_renderer/DirectRenderer.cpp index 8777e84f6..d0892f7ab 100644 --- a/game/graphics/opengl_renderer/DirectRenderer.cpp +++ b/game/graphics/opengl_renderer/DirectRenderer.cpp @@ -392,9 +392,8 @@ void DirectRenderer::update_gl_blend() { glBlendEquation(GL_FUNC_ADD); } else if (state.a == GsAlpha::BlendMode::SOURCE && state.b == GsAlpha::BlendMode::SOURCE && state.c == GsAlpha::BlendMode::SOURCE && state.d == GsAlpha::BlendMode::SOURCE) { - // this is very weird... - glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO); - glBlendEquation(GL_FUNC_ADD); + // trick to disable alpha blending. + glDisable(GL_BLEND); } else if (state.a == GsAlpha::BlendMode::SOURCE && state.b == GsAlpha::BlendMode::ZERO_OR_FIXED && state.c == GsAlpha::BlendMode::DEST && state.d == GsAlpha::BlendMode::DEST) { diff --git a/game/graphics/opengl_renderer/OpenGLRenderer.cpp b/game/graphics/opengl_renderer/OpenGLRenderer.cpp index b53ef1b07..10d6dce74 100644 --- a/game/graphics/opengl_renderer/OpenGLRenderer.cpp +++ b/game/graphics/opengl_renderer/OpenGLRenderer.cpp @@ -303,6 +303,9 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) { { auto prof = m_profiler.root()->make_scoped_child("frame-setup"); setup_frame(settings); + if (settings.gpu_sync) { + glFinish(); + } } { @@ -319,13 +322,16 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) { // render the buckets! { auto prof = m_profiler.root()->make_scoped_child("buckets"); - dispatch_buckets(dma, prof); + dispatch_buckets(dma, prof, settings.gpu_sync); } // apply effects done with PCRTC registers { auto prof = m_profiler.root()->make_scoped_child("pcrtc"); do_pcrtc_effects(settings.pmode_alp_register, &m_render_state, prof); + if (settings.gpu_sync) { + glFinish(); + } } if (settings.draw_render_debug_window) { @@ -333,6 +339,9 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) { draw_renderer_selection_window(); // add a profile bar for the imgui stuff vif_interrupt_callback(); + if (settings.gpu_sync) { + glFinish(); + } } m_last_pmode_alp = settings.pmode_alp_register; @@ -362,8 +371,19 @@ void OpenGLRenderer::render(DmaFollower dma, const RenderOptions& settings) { } if (settings.save_screenshot) { - finish_screenshot(settings.screenshot_path, m_render_state.fbo_state.width, - m_render_state.fbo_state.height, 0, 0, m_render_state.fbo_state.fbo2); + Fbo* screenshot_src; + + // can't screenshot from a multisampled buffer directly - + if (m_fbo_state.resources.resolve_buffer.valid) { + screenshot_src = &m_fbo_state.resources.resolve_buffer; + } else { + screenshot_src = m_fbo_state.render_fbo; + } + finish_screenshot(settings.screenshot_path, screenshot_src->width, screenshot_src->height, 0, 0, + screenshot_src->fbo_id); + } + if (settings.gpu_sync) { + glFinish(); } } @@ -399,113 +419,214 @@ void OpenGLRenderer::draw_renderer_selection_window() { ImGui::End(); } +namespace { +Fbo make_fbo(int w, int h, int msaa, bool make_zbuf_and_stencil) { + Fbo result; + bool use_multisample = msaa > 1; + + // make framebuffer object + glGenFramebuffers(1, &result.fbo_id); + glBindFramebuffer(GL_FRAMEBUFFER, result.fbo_id); + result.valid = true; + + // make texture that will hold the colors of the framebuffer + GLuint tex; + glGenTextures(1, &tex); + result.tex_id = tex; + glActiveTexture(GL_TEXTURE0); + if (use_multisample) { + glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex); + glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, msaa, GL_RGBA8, w, h, GL_TRUE); + } else { + glBindTexture(GL_TEXTURE_2D, tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + } + // make depth and stencil buffers that will hold the... depth and stencil buffers + if (make_zbuf_and_stencil) { + GLuint zbuf; + glGenRenderbuffers(1, &zbuf); + result.zbuf_stencil_id = zbuf; + glBindRenderbuffer(GL_RENDERBUFFER, zbuf); + if (use_multisample) { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_DEPTH24_STENCIL8, w, h); + } else { + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h); + } + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, zbuf); + } + // attach texture to framebuffer as target for colors + + if (use_multisample) { + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + } + + GLenum render_targets[1] = {GL_COLOR_ATTACHMENT0}; + glDrawBuffers(1, render_targets); + auto status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + lg::error("Failed to setup framebuffer: {} {} {} {}\n", w, h, msaa, make_zbuf_and_stencil); + switch (status) { + case GL_FRAMEBUFFER_UNDEFINED: + printf("GL_FRAMEBUFFER_UNDEFINED\n"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + printf("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT\n"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + printf("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT\n"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: + printf("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER\n"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: + printf("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER\n"); + break; + case GL_FRAMEBUFFER_UNSUPPORTED: + printf("GL_FRAMEBUFFER_UNSUPPORTED\n"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: + printf("GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE\n"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: + printf("GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS\n"); + break; + } + ASSERT(false); + } + + result.multisample_count = msaa; + result.multisampled = use_multisample; + result.is_window = false; + result.width = w; + result.height = h; + + return result; +} +} // namespace + /*! * Pre-render frame setup. */ void OpenGLRenderer::setup_frame(const RenderOptions& settings) { - auto& fbo_state = m_render_state.fbo_state; + // glfw controls the window framebuffer, so we just update the size: + auto& window_fb = m_fbo_state.resources.window; + bool window_resized = window_fb.width != settings.window_framebuffer_width || + window_fb.height != settings.window_framebuffer_height; + window_fb.valid = true; + window_fb.is_window = true; + window_fb.fbo_id = 0; + window_fb.width = settings.window_framebuffer_width; + window_fb.height = settings.window_framebuffer_height; + window_fb.multisample_count = 1; + window_fb.multisampled = false; - // restart the buffers if settings changed. - if (settings.game_res_w != fbo_state.width || settings.game_res_h != fbo_state.height || - settings.msaa_samples != fbo_state.msaa) { - fbo_state.delete_objects(); - } + // see if the render FBO is still applicable + if (!m_fbo_state.render_fbo || window_resized || + !m_fbo_state.render_fbo->matches(settings.game_res_w, settings.game_res_h, + settings.msaa_samples)) { + // doesn't match, set up a new one for these settings + lg::info("FBO Setup: requested {}x{}, msaa {}", settings.game_res_w, settings.game_res_h, + settings.msaa_samples); - if (fbo_state.fbo == -1 || fbo_state.fbo2 == -1 || fbo_state.tex == -1 || fbo_state.tex2 == -1 || - fbo_state.zbuf == -1) { - fbo_state.width = settings.game_res_w; - fbo_state.height = settings.game_res_h; - fbo_state.msaa = settings.msaa_samples; + // clear old framebuffers + m_fbo_state.resources.render_buffer.clear(); + m_fbo_state.resources.resolve_buffer.clear(); - bool bad = false; - - // make framebuffer object - if (fbo_state.fbo == -1) { - glGenFramebuffers(1, &fbo_state.fbo); - } - glBindFramebuffer(GL_FRAMEBUFFER, fbo_state.fbo); - - // make texture that will hold the colors of the framebuffer - if (fbo_state.tex == -1) { - glGenTextures(1, &fbo_state.tex); - } - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, fbo_state.tex); - glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, fbo_state.msaa, GL_RGBA8, fbo_state.width, - fbo_state.height, GL_TRUE); - - // make depth and stencil buffers that will hold the... depth and stencil buffers - if (fbo_state.zbuf == -1) { - glGenRenderbuffers(1, &fbo_state.zbuf); - } - glBindRenderbuffer(GL_RENDERBUFFER, fbo_state.zbuf); - glRenderbufferStorageMultisample(GL_RENDERBUFFER, fbo_state.msaa, GL_DEPTH24_STENCIL8, - fbo_state.width, fbo_state.height); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, - fbo_state.zbuf); - - // attach texture to framebuffer as target for colors - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, - fbo_state.tex, 0); - - glDrawBuffers(1, fbo_state.render_targets); - - bad |= glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE; - - // make framebuffer object - if (fbo_state.fbo2 == -1) { - glGenFramebuffers(1, &fbo_state.fbo2); - } - glBindFramebuffer(GL_FRAMEBUFFER, fbo_state.fbo2); - - // make texture that will hold the msaa resolved color buffer - if (fbo_state.tex2 == -1) { - glGenTextures(1, &fbo_state.tex2); - } - glBindTexture(GL_TEXTURE_2D, fbo_state.tex2); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_state.width, fbo_state.height, 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); - - // attach texture to framebuffer as target for colors - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbo_state.tex2, 0); - - glDrawBuffers(1, fbo_state.render_targets); - - bad |= glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE; - - if (!bad) { - glBindFramebuffer(GL_FRAMEBUFFER, fbo_state.fbo); + // first, see if we can just render straight to the display framebuffer. + if (window_fb.matches(settings.game_res_w, settings.game_res_h, settings.msaa_samples)) { + // it matches - no need for extra framebuffers. + lg::info("FBO Setup: rendering directly to window framebuffer"); + m_fbo_state.render_fbo = &m_fbo_state.resources.window; } else { - lg::error("bad framebuffer setup. fbo: {}, tex: {}, zbuf: {}, fbo2: {}, tex2: {}", - fbo_state.fbo, fbo_state.tex, fbo_state.zbuf, fbo_state.fbo2, fbo_state.tex2); - lg::error("size was: {} x {}\n", fbo_state.width, fbo_state.height); - fbo_state.delete_objects(); + lg::info("FBO Setup: window didn't match: {} {}", window_fb.width, window_fb.height); + + // create a fbo to render to, with the desired settings + m_fbo_state.resources.render_buffer = + make_fbo(settings.game_res_w, settings.game_res_h, settings.msaa_samples, true); + m_fbo_state.render_fbo = &m_fbo_state.resources.render_buffer; + + bool resolution_matches = + window_fb.width == settings.game_res_w && window_fb.height == settings.game_res_h; + bool msaa_matches = window_fb.multisample_count == settings.msaa_samples; + + if (!resolution_matches && !msaa_matches) { + lg::info("FBO Setup: using second temporary buffer: res: {}x{} {}x{}", window_fb.width, + window_fb.height, settings.game_res_w, settings.game_res_h); + + // we'll need a temporary fbo to do the msaa resolve step + // non-multisampled, and doesn't need z/stencil + m_fbo_state.resources.resolve_buffer = + make_fbo(settings.game_res_w, settings.game_res_h, 1, false); + } else { + lg::info("FBO Setup: not using second temporary buffer"); + } } - } else { - // we have the objects. bind framebuffer. - glBindFramebuffer(GL_FRAMEBUFFER, fbo_state.fbo); } + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo_state.render_fbo->fbo_id); + ASSERT_MSG(settings.game_res_w > 0 && settings.game_res_h > 0, + fmt::format("Bad viewport size from game_res: {}x{}\n", settings.game_res_w, + settings.game_res_h)); glViewport(0, 0, settings.game_res_w, settings.game_res_h); glClearColor(0.0, 0.0, 0.0, 0.0); glClearDepth(0.0); glDepthMask(GL_TRUE); + // Note: could rely on sky renderer to clear depth and color, but this causes problems with + // letterboxing glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glDisable(GL_BLEND); - m_render_state.window_width_px = settings.window_width_px; - m_render_state.window_height_px = settings.window_height_px; - m_render_state.window_offset_x_px = settings.lbox_width_px; - m_render_state.window_offset_y_px = settings.lbox_height_px; + m_render_state.render_fb_w = settings.game_res_w; + m_render_state.render_fb_h = settings.game_res_h; + + // compare the aspect ratio of the frame buffer to the requested draw area. + float fb_aspect = + (float)settings.window_framebuffer_width / (float)settings.window_framebuffer_height; + float draw_aspect = (float)settings.draw_region_width / (float)settings.draw_region_height; + float squash = fb_aspect / draw_aspect; + + // start with the full area + m_render_state.draw_region_w = m_render_state.render_fb_w; + m_render_state.draw_region_h = m_render_state.render_fb_h; + + // and letterbox as needed + if (squash > 1) { + m_render_state.draw_region_w = ((float)m_render_state.draw_region_w / squash) + 0.1; + } else { + m_render_state.draw_region_h = ((float)m_render_state.draw_region_h * squash) + 0.1; + } + + // center the letterbox + m_render_state.draw_offset_x = (settings.game_res_w - m_render_state.draw_region_w) / 2; + m_render_state.draw_offset_y = (settings.game_res_h - m_render_state.draw_region_h) / 2; + + if (settings.borderless_windows_hacks) { + // pretend the framebuffer is 1 pixel shorter on borderless. fullscreen issues! + // add one pixel of vertical letterbox on borderless to make up for extra line + m_render_state.draw_region_h--; + m_render_state.draw_offset_y++; + } + + m_render_state.render_fb = m_fbo_state.render_fbo->fbo_id; + + if (m_render_state.draw_region_w <= 0 || m_render_state.draw_region_h <= 0) { + // trying to draw to 0 size region... opengl doesn't like this. + m_render_state.draw_region_w = 640; + m_render_state.draw_region_h = 480; + } + glViewport(m_render_state.draw_offset_x, m_render_state.draw_offset_y, + m_render_state.draw_region_w, m_render_state.draw_region_h); } /*! * This function finds buckets and dispatches them to the appropriate part. */ -void OpenGLRenderer::dispatch_buckets(DmaFollower dma, ScopedProfilerNode& prof) { +void OpenGLRenderer::dispatch_buckets(DmaFollower dma, + ScopedProfilerNode& prof, + bool sync_after_buckets) { // The first thing the DMA chain should be a call to a common default-registers chain. // this chain resets the state of the GS. After this is buckets m_category_times.fill(0); @@ -543,6 +664,11 @@ void OpenGLRenderer::dispatch_buckets(DmaFollower dma, ScopedProfilerNode& prof) g_current_render = renderer->name_and_id(); // lg::info("Render: {} start", g_current_render); renderer->render(dma, &m_render_state, bucket_prof); + if (sync_after_buckets) { + auto pp = scoped_prof("finish"); + glFinish(); + } + // lg::info("Render: {} end", g_current_render); // should have ended at the start of the next chain ASSERT(dma.current_tag_offset() == m_render_state.next_bucket); @@ -596,45 +722,54 @@ void OpenGLRenderer::finish_screenshot(const std::string& output_name, void OpenGLRenderer::do_pcrtc_effects(float alp, SharedRenderState* render_state, ScopedProfilerNode& prof) { - int w = render_state->fbo_state.width; - int h = render_state->fbo_state.height; + if (m_fbo_state.render_fbo->is_window) { + // nothing to do! + } else { + Fbo* window_blit_src = nullptr; + const Fbo& window_fbo = m_fbo_state.resources.window; + if (m_fbo_state.resources.resolve_buffer.valid) { + glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo_state.render_fbo->fbo_id); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo_state.resources.resolve_buffer.fbo_id); + glBlitFramebuffer(0, // srcX0 + 0, // srcY0 + m_fbo_state.render_fbo->width, // srcX1 + m_fbo_state.render_fbo->height, // srcY1 + 0, // dstX0 + 0, // dstY0 + m_fbo_state.resources.resolve_buffer.width, // dstX1 + m_fbo_state.resources.resolve_buffer.height, // dstY1 + GL_COLOR_BUFFER_BIT, // mask + GL_LINEAR // filter + ); + window_blit_src = &m_fbo_state.resources.resolve_buffer; + } else { + window_blit_src = &m_fbo_state.resources.render_buffer; + } - // msaa "resolve" - this blit goes from a multisampled framebuffer (fbo/tex) to a normal one - // (fbo2/tex2) - glBindFramebuffer(GL_READ_FRAMEBUFFER, render_state->fbo_state.fbo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, render_state->fbo_state.fbo2); - glBlitFramebuffer(0, // srcX0 - 0, // srcY0 - w, // srcX1 - h, // srcY1 - 0, // dstX0 - 0, // dstY0 - w, // dstX1 - h, // dstY1 - GL_COLOR_BUFFER_BIT, // mask - GL_LINEAR // filter - ); + glBindFramebuffer(GL_READ_FRAMEBUFFER, window_blit_src->fbo_id); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer(0, // srcX0 + 0, // srcY0 + window_blit_src->width, // srcX1 + window_blit_src->height, // srcY1 + 0, // dstX0 + 0, // dstY0 + window_fbo.width, // dstX1 + window_fbo.height, // dstY1 + GL_COLOR_BUFFER_BIT, // mask + GL_LINEAR // filter + ); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + if (alp < 1) { + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + glBlendEquation(GL_FUNC_ADD); + glViewport(0, 0, m_fbo_state.resources.window.width, m_fbo_state.resources.window.height); - // Render the resolved texture to the screen directly now - // TODO: if fbo and the screen have the same resolution, it might be possible to - // glBlitFrameBuffer to the screen directly and skip fbo2/tex2. - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glViewport(render_state->window_offset_x_px, render_state->window_offset_y_px, - render_state->window_width_px, render_state->window_height_px); + m_blackout_renderer.draw(Vector4f(0, 0, 0, 1.f - alp), render_state, prof); - glClearColor(0.0, 0.0, 0.0, 0.0); - glClearDepth(0.0); - glDepthMask(GL_TRUE); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - glDisable(GL_BLEND); - glDisable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); - glBlendEquation(GL_FUNC_ADD); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, render_state->fbo_state.tex2); - - m_blackout_renderer.draw(Vector4f(0, 0, 0, alp), render_state, prof); - - glEnable(GL_DEPTH_TEST); + glEnable(GL_DEPTH_TEST); + } } diff --git a/game/graphics/opengl_renderer/OpenGLRenderer.h b/game/graphics/opengl_renderer/OpenGLRenderer.h index 975af0ed3..dc974a773 100644 --- a/game/graphics/opengl_renderer/OpenGLRenderer.h +++ b/game/graphics/opengl_renderer/OpenGLRenderer.h @@ -13,23 +13,80 @@ #include "game/tools/subtitles/subtitle_editor.h" struct RenderOptions { - int window_height_px = 0; - int window_width_px = 0; - int lbox_height_px = 0; - int lbox_width_px = 0; bool draw_render_debug_window = false; bool draw_profiler_window = false; bool draw_small_profiler_window = false; bool draw_subtitle_editor_window = false; + // internal rendering settings - The OpenGLRenderer will internally use this resolution/format. int msaa_samples = 4; int game_res_w = 640; int game_res_h = 480; + // size of the window's framebuffer (framebuffer 0) + // The renderer needs to know this to do an optimization to render directly to the window's + // framebuffer when possible. + int window_framebuffer_height = 0; + int window_framebuffer_width = 0; + + // the part of the window that we should draw to. The rest is black. This value is determined by + // logic inside of the game - it needs to know the desired aspect ratio. + int draw_region_height = 0; + int draw_region_width = 0; + + // windows-specific tweaks to the size of the drawing area in borderless. + bool borderless_windows_hacks = false; + bool save_screenshot = false; std::string screenshot_path; float pmode_alp_register = 0.f; + + // when enabled, does a `glFinish()` after each major rendering pass. This blocks until the GPU + // is done working, making it easier to profile GPU utilization. + bool gpu_sync = false; +}; + +struct Fbo { + bool valid = false; // do we have an OpenGL fbo_id? + GLuint fbo_id = -1; + + // optional rgba/zbuffer/stencil data. + std::optional tex_id; + std::optional zbuf_stencil_id; + + bool multisampled = false; + int multisample_count = 0; // Should be 1 if multisampled is disabled + + bool is_window = false; + int width = 640; + int height = 480; + + // Does this fbo match the given format? MSAA = 1 will accept a normal buffer, or a multisample 1x + bool matches(int w, int h, int msaa) const { + int effective_msaa = multisampled ? multisample_count : 1; + return valid && width == w && height == h && effective_msaa == msaa; + } + + // Free opengl resources, if we have any. + void clear() { + if (valid) { + glDeleteFramebuffers(1, &fbo_id); + fbo_id = -1; + + if (tex_id) { + glDeleteTextures(1, &tex_id.value()); + tex_id.reset(); + } + + if (zbuf_stencil_id) { + glDeleteRenderbuffers(1, &zbuf_stencil_id.value()); + zbuf_stencil_id.reset(); + } + + valid = false; + } + } }; /*! @@ -49,7 +106,7 @@ class OpenGLRenderer { private: void setup_frame(const RenderOptions& settings); - void dispatch_buckets(DmaFollower dma, ScopedProfilerNode& prof); + void dispatch_buckets(DmaFollower dma, ScopedProfilerNode& prof, bool sync_after_buckets); void do_pcrtc_effects(float alp, SharedRenderState* render_state, ScopedProfilerNode& prof); void init_bucket_renderers(); void draw_renderer_selection_window(); @@ -80,4 +137,14 @@ class OpenGLRenderer { float m_last_pmode_alp = 1.; bool m_enable_fast_blackout_loads = true; + + struct FboState { + struct { + Fbo window; // provided by glfw + Fbo render_buffer; // temporary buffer to render to + Fbo resolve_buffer; // temporary buffer to resolve to + } resources; + + Fbo* render_fbo = nullptr; // the selected fbo from the three above to use for rendering + } m_fbo_state; }; diff --git a/game/graphics/opengl_renderer/Sprite3.cpp b/game/graphics/opengl_renderer/Sprite3.cpp index 7ca1912d0..67976b2b2 100644 --- a/game/graphics/opengl_renderer/Sprite3.cpp +++ b/game/graphics/opengl_renderer/Sprite3.cpp @@ -711,22 +711,22 @@ void Sprite3::distort_draw_instanced(SharedRenderState* render_state, ScopedProf void Sprite3::distort_draw_common(SharedRenderState* render_state, ScopedProfilerNode& /*prof*/) { // The distort effect needs to read the current framebuffer, so copy what's been rendered so far // to a texture that we can then pass to the shader - glBindFramebuffer(GL_READ_FRAMEBUFFER, render_state->fbo_state.fbo); + glBindFramebuffer(GL_READ_FRAMEBUFFER, render_state->render_fb); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_distort_ogl.fbo); - glBlitFramebuffer(0, // srcX0 - 0, // srcY0 - render_state->fbo_state.width, // srcX1 - render_state->fbo_state.height, // srcY1 - 0, // dstX0 - 0, // dstY0 - m_distort_ogl.fbo_width, // dstX1 - m_distort_ogl.fbo_height, // dstY1 - GL_COLOR_BUFFER_BIT, // mask - GL_NEAREST // filter + glBlitFramebuffer(render_state->draw_offset_x, // srcX0 + render_state->draw_offset_y, // srcY0 + render_state->draw_offset_x + render_state->draw_region_w, // srcX1 + render_state->draw_offset_y + render_state->draw_region_h, // srcY1 + 0, // dstX0 + 0, // dstY0 + m_distort_ogl.fbo_width, // dstX1 + m_distort_ogl.fbo_height, // dstY1 + GL_COLOR_BUFFER_BIT, // mask + GL_NEAREST // filter ); - glBindFramebuffer(GL_FRAMEBUFFER, render_state->fbo_state.fbo); + glBindFramebuffer(GL_FRAMEBUFFER, render_state->render_fb); // Set up OpenGL state m_current_mode.set_depth_write_enable(!m_sprite_distorter_setup.zbuf.zmsk()); // zbuf @@ -741,10 +741,10 @@ void Sprite3::distort_draw_common(SharedRenderState* render_state, ScopedProfile void Sprite3::distort_setup_framebuffer_dims(SharedRenderState* render_state) { // Distort framebuffer must be the same dimensions as the default window framebuffer - if (m_distort_ogl.fbo_width != render_state->fbo_state.width || - m_distort_ogl.fbo_height != render_state->fbo_state.height) { - m_distort_ogl.fbo_width = render_state->fbo_state.width; - m_distort_ogl.fbo_height = render_state->fbo_state.height; + if (m_distort_ogl.fbo_width != render_state->draw_region_w || + m_distort_ogl.fbo_height != render_state->draw_region_h) { + m_distort_ogl.fbo_width = render_state->draw_region_w; + m_distort_ogl.fbo_height = render_state->draw_region_h; glBindTexture(GL_TEXTURE_2D, m_distort_ogl.fbo_texture); diff --git a/game/graphics/opengl_renderer/opengl_utils.cpp b/game/graphics/opengl_renderer/opengl_utils.cpp index 0d181f824..e322a8cf8 100644 --- a/game/graphics/opengl_renderer/opengl_utils.cpp +++ b/game/graphics/opengl_renderer/opengl_utils.cpp @@ -140,7 +140,7 @@ void FullScreenDraw::draw(const math::Vector4f& color, ScopedProfilerNode& prof) { glBindVertexArray(m_vao); glBindBuffer(GL_ARRAY_BUFFER, m_vertex_buffer); - auto& shader = render_state->shaders[ShaderId::POST_PROCESSING]; + auto& shader = render_state->shaders[ShaderId::SOLID_COLOR]; shader.activate(); glUniform4f(glGetUniformLocation(shader.id(), "fragment_color"), color[0], color[1], color[2], color[3]); diff --git a/game/graphics/pipelines/opengl.cpp b/game/graphics/pipelines/opengl.cpp index 7f7f07d88..46f3a7104 100644 --- a/game/graphics/pipelines/opengl.cpp +++ b/game/graphics/pipelines/opengl.cpp @@ -259,11 +259,12 @@ static bool endsWith(std::string_view str, std::string_view suffix) { void render_game_frame(int game_width, int game_height, - int window_width, - int window_height, - int lbox_width, - int lbox_height, - int msaa_samples) { + int window_fb_width, + int window_fb_height, + int draw_region_width, + int draw_region_height, + int msaa_samples, + bool windows_borderless_hack) { // wait for a copied chain. bool got_chain = false; { @@ -280,19 +281,23 @@ void render_game_frame(int game_width, RenderOptions options; options.game_res_w = game_width; options.game_res_h = game_height; - options.window_height_px = window_height; - options.window_width_px = window_width; - options.lbox_height_px = lbox_height; - options.lbox_width_px = lbox_width; + options.window_framebuffer_width = window_fb_width; + options.window_framebuffer_height = window_fb_height; + options.draw_region_height = draw_region_height; + options.draw_region_width = draw_region_width; options.msaa_samples = msaa_samples; options.draw_render_debug_window = g_gfx_data->debug_gui.should_draw_render_debug(); options.draw_profiler_window = g_gfx_data->debug_gui.should_draw_profiler(); options.draw_subtitle_editor_window = g_gfx_data->debug_gui.should_draw_subtitle_editor(); options.save_screenshot = false; + options.gpu_sync = g_gfx_data->debug_gui.should_gl_finish(); + options.borderless_windows_hacks = windows_borderless_hack; if (g_gfx_data->debug_gui.get_screenshot_flag()) { options.save_screenshot = true; options.game_res_w = g_gfx_data->debug_gui.screenshot_width; options.game_res_h = g_gfx_data->debug_gui.screenshot_height; + options.draw_region_width = options.game_res_w; + options.draw_region_height = options.game_res_h; options.msaa_samples = g_gfx_data->debug_gui.screenshot_samples; } options.draw_small_profiler_window = g_gfx_data->debug_gui.small_profiler; @@ -487,38 +492,30 @@ void GLDisplay::render() { ImGui::NewFrame(); } - // window size - int win_w = Gfx::g_global_settings.lbox_w; - int win_h = Gfx::g_global_settings.lbox_h; + // framebuffer size int fbuf_w, fbuf_h; glfwGetFramebufferSize(m_window, &fbuf_w, &fbuf_h); + bool windows_borderless_hacks = false; #ifdef _WIN32 if (last_fullscreen_mode() == GfxDisplayMode::Borderless) { - // pretend the framebuffer is 1 pixel shorter on borderless. fullscreen issues! - fbuf_h--; - } -#endif - // horizontal letterbox size - int lbox_w = (fbuf_w - win_w) / 2; - // vertical letterbox size - int lbox_h = (fbuf_h - win_h) / 2; -#ifdef _WIN32 - if (last_fullscreen_mode() == GfxDisplayMode::Borderless) { - // add one pixel of vertical letterbox on borderless to make up for extra line - lbox_h++; + windows_borderless_hacks = true; } #endif // render game! if (g_gfx_data->debug_gui.should_advance_frame()) { auto p = scoped_prof("game-render"); - render_game_frame(Gfx::g_global_settings.game_res_w, Gfx::g_global_settings.game_res_h, win_w, - win_h, lbox_w, lbox_h, Gfx::g_global_settings.msaa_samples); - } - - if (g_gfx_data->debug_gui.should_gl_finish()) { - auto p = scoped_prof("gl-finish"); - glFinish(); + int game_res_w = Gfx::g_global_settings.game_res_w; + int game_res_h = Gfx::g_global_settings.game_res_h; + if (game_res_w <= 0 || game_res_h <= 0) { + // if the window size reports 0, the game will ask for a 0 sized window, and nothing likes + // that. + game_res_w = 640; + game_res_h = 480; + } + render_game_frame(game_res_w, game_res_h, fbuf_w, fbuf_h, Gfx::g_global_settings.lbox_w, + Gfx::g_global_settings.lbox_h, Gfx::g_global_settings.msaa_samples, + windows_borderless_hacks); } // render debug @@ -550,33 +547,51 @@ void GLDisplay::render() { Gfx::g_global_settings.target_fps, Gfx::g_global_settings.experimental_accurate_lag, Gfx::g_global_settings.sleep_in_frame_limiter, g_gfx_data->last_engine_time); } + // actually wait for vsync + if (g_gfx_data->debug_gui.should_gl_finish()) { + glFinish(); + } + + // Start timing for the next frame. g_gfx_data->debug_gui.start_frame(); prof().instant_event("ROOT"); update_global_profiler(); - if (!minimized() && fullscreen_pending()) { - fullscreen_flush(); - } - update_last_fullscreen_mode(); - // toggle even odd and wake up engine waiting on vsync. + // TODO: we could play with moving this earlier, right after the final bucket renderer. + // it breaks the VIF-interrupt profiling though. { + prof().instant_event("engine-notify"); std::unique_lock lock(g_gfx_data->sync_mutex); g_gfx_data->frame_idx++; g_gfx_data->sync_cv.notify_all(); } + { + auto p = scoped_prof("fullscreen-update"); + // slow, takes ~0.15 ms on linux + auto current_fullscreen_mode = get_fullscreen(); + // checking minimized also takes ~0.1 ms, only check if we need to update fullscreen modes + if (current_fullscreen_mode != m_fullscreen_target_mode && !minimized()) { + fullscreen_flush(); + } + m_last_fullscreen_mode = current_fullscreen_mode; + } + // reboot whole game, if requested if (g_gfx_data->debug_gui.want_reboot_in_debug) { g_gfx_data->debug_gui.want_reboot_in_debug = false; MasterExit = RuntimeExitStatus::RESTART_IN_DEBUG; } - // exit if display window was closed - if (glfwWindowShouldClose(m_window)) { - std::unique_lock lock(g_gfx_data->sync_mutex); - MasterExit = RuntimeExitStatus::EXIT; - g_gfx_data->sync_cv.notify_all(); + { + auto p = scoped_prof("check-close-window"); + // exit if display window was closed + if (glfwWindowShouldClose(m_window)) { + std::unique_lock lock(g_gfx_data->sync_mutex); + MasterExit = RuntimeExitStatus::EXIT; + g_gfx_data->sync_cv.notify_all(); + } } }