diff options
| author | 2020-09-21 00:21:32 +0000 | |
|---|---|---|
| committer | 2020-09-21 00:21:32 +0000 | |
| commit | a2eb44db825a892cc2863bd1f5d0352c273ff0f0 (patch) | |
| tree | 3e75441609f97c8a6ad01e70461ecb218c51d086 | |
| parent | Merge pull request #4683 from Morph1984/NpadHandheldActivationMode-impl (diff) | |
| parent | renderer_opengl: Remove emulated mailbox presentation (diff) | |
| download | yuzu-a2eb44db825a892cc2863bd1f5d0352c273ff0f0.tar.gz yuzu-a2eb44db825a892cc2863bd1f5d0352c273ff0f0.tar.xz yuzu-a2eb44db825a892cc2863bd1f5d0352c273ff0f0.zip | |
Merge pull request #4692 from ReinUsesLisp/remove-vsync
renderer_opengl: Remove emulated mailbox presentation
Diffstat (limited to '')
| -rw-r--r-- | src/video_core/renderer_base.h | 5 | ||||
| -rw-r--r-- | src/video_core/renderer_opengl/renderer_opengl.cpp | 288 | ||||
| -rw-r--r-- | src/video_core/renderer_opengl/renderer_opengl.h | 16 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/renderer_vulkan.cpp | 5 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/renderer_vulkan.h | 1 | ||||
| -rw-r--r-- | src/yuzu/bootmanager.cpp | 23 | ||||
| -rw-r--r-- | src/yuzu_cmd/emu_window/emu_window_sdl2.cpp | 5 | ||||
| -rw-r--r-- | src/yuzu_cmd/emu_window/emu_window_sdl2.h | 9 | ||||
| -rw-r--r-- | src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp | 15 | ||||
| -rw-r--r-- | src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h | 5 | ||||
| -rw-r--r-- | src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp | 9 | ||||
| -rw-r--r-- | src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h | 7 | ||||
| -rw-r--r-- | src/yuzu_cmd/yuzu.cpp | 7 |
13 files changed, 35 insertions, 360 deletions
diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h index 649074acd..5c650808b 100644 --- a/src/video_core/renderer_base.h +++ b/src/video_core/renderer_base.h | |||
| @@ -46,11 +46,6 @@ public: | |||
| 46 | /// Finalize rendering the guest frame and draw into the presentation texture | 46 | /// Finalize rendering the guest frame and draw into the presentation texture |
| 47 | virtual void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) = 0; | 47 | virtual void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) = 0; |
| 48 | 48 | ||
| 49 | /// Draws the latest frame to the window waiting timeout_ms for a frame to arrive (Renderer | ||
| 50 | /// specific implementation) | ||
| 51 | /// Returns true if a frame was drawn | ||
| 52 | virtual bool TryPresent(int timeout_ms) = 0; | ||
| 53 | |||
| 54 | // Getter/setter functions: | 49 | // Getter/setter functions: |
| 55 | // ------------------------ | 50 | // ------------------------ |
| 56 | 51 | ||
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index a4c5b8f74..2ccca1993 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp | |||
| @@ -32,20 +32,6 @@ namespace OpenGL { | |||
| 32 | 32 | ||
| 33 | namespace { | 33 | namespace { |
| 34 | 34 | ||
| 35 | constexpr std::size_t SWAP_CHAIN_SIZE = 3; | ||
| 36 | |||
| 37 | struct Frame { | ||
| 38 | u32 width{}; /// Width of the frame (to detect resize) | ||
| 39 | u32 height{}; /// Height of the frame | ||
| 40 | bool color_reloaded{}; /// Texture attachment was recreated (ie: resized) | ||
| 41 | OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO | ||
| 42 | OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread | ||
| 43 | OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread | ||
| 44 | GLsync render_fence{}; /// Fence created on the render thread | ||
| 45 | GLsync present_fence{}; /// Fence created on the presentation thread | ||
| 46 | bool is_srgb{}; /// Framebuffer is sRGB or RGB | ||
| 47 | }; | ||
| 48 | |||
| 49 | constexpr GLint PositionLocation = 0; | 35 | constexpr GLint PositionLocation = 0; |
| 50 | constexpr GLint TexCoordLocation = 1; | 36 | constexpr GLint TexCoordLocation = 1; |
| 51 | constexpr GLint ModelViewMatrixLocation = 0; | 37 | constexpr GLint ModelViewMatrixLocation = 0; |
| @@ -58,24 +44,6 @@ struct ScreenRectVertex { | |||
| 58 | std::array<GLfloat, 2> tex_coord; | 44 | std::array<GLfloat, 2> tex_coord; |
| 59 | }; | 45 | }; |
| 60 | 46 | ||
| 61 | /// Returns true if any debug tool is attached | ||
| 62 | bool HasDebugTool() { | ||
| 63 | const bool nsight = std::getenv("NVTX_INJECTION64_PATH") || std::getenv("NSIGHT_LAUNCHED"); | ||
| 64 | if (nsight) { | ||
| 65 | return true; | ||
| 66 | } | ||
| 67 | |||
| 68 | GLint num_extensions; | ||
| 69 | glGetIntegerv(GL_NUM_EXTENSIONS, &num_extensions); | ||
| 70 | for (GLuint index = 0; index < static_cast<GLuint>(num_extensions); ++index) { | ||
| 71 | const auto name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, index)); | ||
| 72 | if (!std::strcmp(name, "GL_EXT_debug_tool")) { | ||
| 73 | return true; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | return false; | ||
| 77 | } | ||
| 78 | |||
| 79 | /** | 47 | /** |
| 80 | * Defines a 1:1 pixel ortographic projection matrix with (0,0) on the top-left | 48 | * Defines a 1:1 pixel ortographic projection matrix with (0,0) on the top-left |
| 81 | * corner and (width, height) on the lower-bottom. | 49 | * corner and (width, height) on the lower-bottom. |
| @@ -159,135 +127,15 @@ void APIENTRY DebugHandler(GLenum source, GLenum type, GLuint id, GLenum severit | |||
| 159 | 127 | ||
| 160 | } // Anonymous namespace | 128 | } // Anonymous namespace |
| 161 | 129 | ||
| 162 | /** | ||
| 163 | * For smooth Vsync rendering, we want to always present the latest frame that the core generates, | ||
| 164 | * but also make sure that rendering happens at the pace that the frontend dictates. This is a | ||
| 165 | * helper class that the renderer uses to sync frames between the render thread and the presentation | ||
| 166 | * thread | ||
| 167 | */ | ||
| 168 | class FrameMailbox { | ||
| 169 | public: | ||
| 170 | std::mutex swap_chain_lock; | ||
| 171 | std::condition_variable present_cv; | ||
| 172 | std::array<Frame, SWAP_CHAIN_SIZE> swap_chain{}; | ||
| 173 | std::queue<Frame*> free_queue; | ||
| 174 | std::deque<Frame*> present_queue; | ||
| 175 | Frame* previous_frame{}; | ||
| 176 | |||
| 177 | FrameMailbox() { | ||
| 178 | for (auto& frame : swap_chain) { | ||
| 179 | free_queue.push(&frame); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | ~FrameMailbox() { | ||
| 184 | // lock the mutex and clear out the present and free_queues and notify any people who are | ||
| 185 | // blocked to prevent deadlock on shutdown | ||
| 186 | std::scoped_lock lock{swap_chain_lock}; | ||
| 187 | std::queue<Frame*>().swap(free_queue); | ||
| 188 | present_queue.clear(); | ||
| 189 | present_cv.notify_all(); | ||
| 190 | } | ||
| 191 | |||
| 192 | void ReloadPresentFrame(Frame* frame, u32 height, u32 width) { | ||
| 193 | frame->present.Release(); | ||
| 194 | frame->present.Create(); | ||
| 195 | GLint previous_draw_fbo{}; | ||
| 196 | glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_fbo); | ||
| 197 | glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle); | ||
| 198 | glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, | ||
| 199 | frame->color.handle); | ||
| 200 | if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { | ||
| 201 | LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!"); | ||
| 202 | } | ||
| 203 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo); | ||
| 204 | frame->color_reloaded = false; | ||
| 205 | } | ||
| 206 | |||
| 207 | void ReloadRenderFrame(Frame* frame, u32 width, u32 height) { | ||
| 208 | // Recreate the color texture attachment | ||
| 209 | frame->color.Release(); | ||
| 210 | frame->color.Create(); | ||
| 211 | const GLenum internal_format = frame->is_srgb ? GL_SRGB8 : GL_RGB8; | ||
| 212 | glNamedRenderbufferStorage(frame->color.handle, internal_format, width, height); | ||
| 213 | |||
| 214 | // Recreate the FBO for the render target | ||
| 215 | frame->render.Release(); | ||
| 216 | frame->render.Create(); | ||
| 217 | glBindFramebuffer(GL_FRAMEBUFFER, frame->render.handle); | ||
| 218 | glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, | ||
| 219 | frame->color.handle); | ||
| 220 | if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { | ||
| 221 | LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!"); | ||
| 222 | } | ||
| 223 | |||
| 224 | frame->width = width; | ||
| 225 | frame->height = height; | ||
| 226 | frame->color_reloaded = true; | ||
| 227 | } | ||
| 228 | |||
| 229 | Frame* GetRenderFrame() { | ||
| 230 | std::unique_lock lock{swap_chain_lock}; | ||
| 231 | |||
| 232 | // If theres no free frames, we will reuse the oldest render frame | ||
| 233 | if (free_queue.empty()) { | ||
| 234 | auto frame = present_queue.back(); | ||
| 235 | present_queue.pop_back(); | ||
| 236 | return frame; | ||
| 237 | } | ||
| 238 | |||
| 239 | Frame* frame = free_queue.front(); | ||
| 240 | free_queue.pop(); | ||
| 241 | return frame; | ||
| 242 | } | ||
| 243 | |||
| 244 | void ReleaseRenderFrame(Frame* frame) { | ||
| 245 | std::unique_lock lock{swap_chain_lock}; | ||
| 246 | present_queue.push_front(frame); | ||
| 247 | present_cv.notify_one(); | ||
| 248 | } | ||
| 249 | |||
| 250 | Frame* TryGetPresentFrame(int timeout_ms) { | ||
| 251 | std::unique_lock lock{swap_chain_lock}; | ||
| 252 | // wait for new entries in the present_queue | ||
| 253 | present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), | ||
| 254 | [&] { return !present_queue.empty(); }); | ||
| 255 | if (present_queue.empty()) { | ||
| 256 | // timed out waiting for a frame to draw so return the previous frame | ||
| 257 | return previous_frame; | ||
| 258 | } | ||
| 259 | |||
| 260 | // free the previous frame and add it back to the free queue | ||
| 261 | if (previous_frame) { | ||
| 262 | free_queue.push(previous_frame); | ||
| 263 | } | ||
| 264 | |||
| 265 | // the newest entries are pushed to the front of the queue | ||
| 266 | Frame* frame = present_queue.front(); | ||
| 267 | present_queue.pop_front(); | ||
| 268 | // remove all old entries from the present queue and move them back to the free_queue | ||
| 269 | for (auto f : present_queue) { | ||
| 270 | free_queue.push(f); | ||
| 271 | } | ||
| 272 | present_queue.clear(); | ||
| 273 | previous_frame = frame; | ||
| 274 | return frame; | ||
| 275 | } | ||
| 276 | }; | ||
| 277 | |||
| 278 | RendererOpenGL::RendererOpenGL(Core::TelemetrySession& telemetry_session_, | 130 | RendererOpenGL::RendererOpenGL(Core::TelemetrySession& telemetry_session_, |
| 279 | Core::Frontend::EmuWindow& emu_window_, | 131 | Core::Frontend::EmuWindow& emu_window_, |
| 280 | Core::Memory::Memory& cpu_memory_, Tegra::GPU& gpu_, | 132 | Core::Memory::Memory& cpu_memory_, Tegra::GPU& gpu_, |
| 281 | std::unique_ptr<Core::Frontend::GraphicsContext> context) | 133 | std::unique_ptr<Core::Frontend::GraphicsContext> context) |
| 282 | : RendererBase{emu_window_, std::move(context)}, telemetry_session{telemetry_session_}, | 134 | : RendererBase{emu_window_, std::move(context)}, telemetry_session{telemetry_session_}, |
| 283 | emu_window{emu_window_}, cpu_memory{cpu_memory_}, gpu{gpu_}, program_manager{device}, | 135 | emu_window{emu_window_}, cpu_memory{cpu_memory_}, gpu{gpu_}, program_manager{device} {} |
| 284 | has_debug_tool{HasDebugTool()} {} | ||
| 285 | 136 | ||
| 286 | RendererOpenGL::~RendererOpenGL() = default; | 137 | RendererOpenGL::~RendererOpenGL() = default; |
| 287 | 138 | ||
| 288 | MICROPROFILE_DEFINE(OpenGL_RenderFrame, "OpenGL", "Render Frame", MP_RGB(128, 128, 64)); | ||
| 289 | MICROPROFILE_DEFINE(OpenGL_WaitPresent, "OpenGL", "Wait For Present", MP_RGB(128, 128, 128)); | ||
| 290 | |||
| 291 | void RendererOpenGL::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) { | 139 | void RendererOpenGL::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) { |
| 292 | if (!framebuffer) { | 140 | if (!framebuffer) { |
| 293 | return; | 141 | return; |
| @@ -296,79 +144,34 @@ void RendererOpenGL::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) { | |||
| 296 | PrepareRendertarget(framebuffer); | 144 | PrepareRendertarget(framebuffer); |
| 297 | RenderScreenshot(); | 145 | RenderScreenshot(); |
| 298 | 146 | ||
| 299 | Frame* frame; | 147 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); |
| 300 | { | 148 | DrawScreen(emu_window.GetFramebufferLayout()); |
| 301 | MICROPROFILE_SCOPE(OpenGL_WaitPresent); | ||
| 302 | |||
| 303 | frame = frame_mailbox->GetRenderFrame(); | ||
| 304 | |||
| 305 | // Clean up sync objects before drawing | ||
| 306 | |||
| 307 | // INTEL driver workaround. We can't delete the previous render sync object until we are | ||
| 308 | // sure that the presentation is done | ||
| 309 | if (frame->present_fence) { | ||
| 310 | glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); | ||
| 311 | } | ||
| 312 | |||
| 313 | // delete the draw fence if the frame wasn't presented | ||
| 314 | if (frame->render_fence) { | ||
| 315 | glDeleteSync(frame->render_fence); | ||
| 316 | frame->render_fence = 0; | ||
| 317 | } | ||
| 318 | |||
| 319 | // wait for the presentation to be done | ||
| 320 | if (frame->present_fence) { | ||
| 321 | glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); | ||
| 322 | glDeleteSync(frame->present_fence); | ||
| 323 | frame->present_fence = 0; | ||
| 324 | } | ||
| 325 | } | ||
| 326 | 149 | ||
| 327 | { | 150 | ++m_current_frame; |
| 328 | MICROPROFILE_SCOPE(OpenGL_RenderFrame); | ||
| 329 | const auto& layout = render_window.GetFramebufferLayout(); | ||
| 330 | 151 | ||
| 331 | // Recreate the frame if the size of the window has changed | 152 | rasterizer->TickFrame(); |
| 332 | if (layout.width != frame->width || layout.height != frame->height || | ||
| 333 | screen_info.display_srgb != frame->is_srgb) { | ||
| 334 | LOG_DEBUG(Render_OpenGL, "Reloading render frame"); | ||
| 335 | frame->is_srgb = screen_info.display_srgb; | ||
| 336 | frame_mailbox->ReloadRenderFrame(frame, layout.width, layout.height); | ||
| 337 | } | ||
| 338 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame->render.handle); | ||
| 339 | DrawScreen(layout); | ||
| 340 | // Create a fence for the frontend to wait on and swap this frame to OffTex | ||
| 341 | frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); | ||
| 342 | glFlush(); | ||
| 343 | frame_mailbox->ReleaseRenderFrame(frame); | ||
| 344 | m_current_frame++; | ||
| 345 | rasterizer->TickFrame(); | ||
| 346 | } | ||
| 347 | 153 | ||
| 348 | render_window.PollEvents(); | 154 | render_window.PollEvents(); |
| 349 | if (has_debug_tool) { | 155 | context->SwapBuffers(); |
| 350 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); | ||
| 351 | Present(0); | ||
| 352 | context->SwapBuffers(); | ||
| 353 | } | ||
| 354 | } | 156 | } |
| 355 | 157 | ||
| 356 | void RendererOpenGL::PrepareRendertarget(const Tegra::FramebufferConfig* framebuffer) { | 158 | void RendererOpenGL::PrepareRendertarget(const Tegra::FramebufferConfig* framebuffer) { |
| 357 | if (framebuffer) { | 159 | if (!framebuffer) { |
| 358 | // If framebuffer is provided, reload it from memory to a texture | 160 | return; |
| 359 | if (screen_info.texture.width != static_cast<GLsizei>(framebuffer->width) || | 161 | } |
| 360 | screen_info.texture.height != static_cast<GLsizei>(framebuffer->height) || | 162 | // If framebuffer is provided, reload it from memory to a texture |
| 361 | screen_info.texture.pixel_format != framebuffer->pixel_format || | 163 | if (screen_info.texture.width != static_cast<GLsizei>(framebuffer->width) || |
| 362 | gl_framebuffer_data.empty()) { | 164 | screen_info.texture.height != static_cast<GLsizei>(framebuffer->height) || |
| 363 | // Reallocate texture if the framebuffer size has changed. | 165 | screen_info.texture.pixel_format != framebuffer->pixel_format || |
| 364 | // This is expected to not happen very often and hence should not be a | 166 | gl_framebuffer_data.empty()) { |
| 365 | // performance problem. | 167 | // Reallocate texture if the framebuffer size has changed. |
| 366 | ConfigureFramebufferTexture(screen_info.texture, *framebuffer); | 168 | // This is expected to not happen very often and hence should not be a |
| 367 | } | 169 | // performance problem. |
| 368 | 170 | ConfigureFramebufferTexture(screen_info.texture, *framebuffer); | |
| 369 | // Load the framebuffer from memory, draw it to the screen, and swap buffers | ||
| 370 | LoadFBToScreenInfo(*framebuffer); | ||
| 371 | } | 171 | } |
| 172 | |||
| 173 | // Load the framebuffer from memory, draw it to the screen, and swap buffers | ||
| 174 | LoadFBToScreenInfo(*framebuffer); | ||
| 372 | } | 175 | } |
| 373 | 176 | ||
| 374 | void RendererOpenGL::LoadFBToScreenInfo(const Tegra::FramebufferConfig& framebuffer) { | 177 | void RendererOpenGL::LoadFBToScreenInfo(const Tegra::FramebufferConfig& framebuffer) { |
| @@ -418,8 +221,6 @@ void RendererOpenGL::LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color | |||
| 418 | } | 221 | } |
| 419 | 222 | ||
| 420 | void RendererOpenGL::InitOpenGLObjects() { | 223 | void RendererOpenGL::InitOpenGLObjects() { |
| 421 | frame_mailbox = std::make_unique<FrameMailbox>(); | ||
| 422 | |||
| 423 | glClearColor(Settings::values.bg_red.GetValue(), Settings::values.bg_green.GetValue(), | 224 | glClearColor(Settings::values.bg_red.GetValue(), Settings::values.bg_green.GetValue(), |
| 424 | Settings::values.bg_blue.GetValue(), 0.0f); | 225 | Settings::values.bg_blue.GetValue(), 0.0f); |
| 425 | 226 | ||
| @@ -647,51 +448,6 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) { | |||
| 647 | program_manager.RestoreGuestPipeline(); | 448 | program_manager.RestoreGuestPipeline(); |
| 648 | } | 449 | } |
| 649 | 450 | ||
| 650 | bool RendererOpenGL::TryPresent(int timeout_ms) { | ||
| 651 | if (has_debug_tool) { | ||
| 652 | LOG_DEBUG(Render_OpenGL, | ||
| 653 | "Skipping presentation because we are presenting on the main context"); | ||
| 654 | return false; | ||
| 655 | } | ||
| 656 | return Present(timeout_ms); | ||
| 657 | } | ||
| 658 | |||
| 659 | bool RendererOpenGL::Present(int timeout_ms) { | ||
| 660 | const auto& layout = render_window.GetFramebufferLayout(); | ||
| 661 | auto frame = frame_mailbox->TryGetPresentFrame(timeout_ms); | ||
| 662 | if (!frame) { | ||
| 663 | LOG_DEBUG(Render_OpenGL, "TryGetPresentFrame returned no frame to present"); | ||
| 664 | return false; | ||
| 665 | } | ||
| 666 | |||
| 667 | // Clearing before a full overwrite of a fbo can signal to drivers that they can avoid a | ||
| 668 | // readback since we won't be doing any blending | ||
| 669 | glClear(GL_COLOR_BUFFER_BIT); | ||
| 670 | |||
| 671 | // Recreate the presentation FBO if the color attachment was changed | ||
| 672 | if (frame->color_reloaded) { | ||
| 673 | LOG_DEBUG(Render_OpenGL, "Reloading present frame"); | ||
| 674 | frame_mailbox->ReloadPresentFrame(frame, layout.width, layout.height); | ||
| 675 | } | ||
| 676 | glWaitSync(frame->render_fence, 0, GL_TIMEOUT_IGNORED); | ||
| 677 | // INTEL workaround. | ||
| 678 | // Normally we could just delete the draw fence here, but due to driver bugs, we can just delete | ||
| 679 | // it on the emulation thread without too much penalty | ||
| 680 | // glDeleteSync(frame.render_sync); | ||
| 681 | // frame.render_sync = 0; | ||
| 682 | |||
| 683 | glBindFramebuffer(GL_READ_FRAMEBUFFER, frame->present.handle); | ||
| 684 | glBlitFramebuffer(0, 0, frame->width, frame->height, 0, 0, layout.width, layout.height, | ||
| 685 | GL_COLOR_BUFFER_BIT, GL_LINEAR); | ||
| 686 | |||
| 687 | // Insert fence for the main thread to block on | ||
| 688 | frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); | ||
| 689 | glFlush(); | ||
| 690 | |||
| 691 | glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); | ||
| 692 | return true; | ||
| 693 | } | ||
| 694 | |||
| 695 | void RendererOpenGL::RenderScreenshot() { | 451 | void RendererOpenGL::RenderScreenshot() { |
| 696 | if (!renderer_settings.screenshot_requested) { | 452 | if (!renderer_settings.screenshot_requested) { |
| 697 | return; | 453 | return; |
| @@ -706,7 +462,7 @@ void RendererOpenGL::RenderScreenshot() { | |||
| 706 | screenshot_framebuffer.Create(); | 462 | screenshot_framebuffer.Create(); |
| 707 | glBindFramebuffer(GL_FRAMEBUFFER, screenshot_framebuffer.handle); | 463 | glBindFramebuffer(GL_FRAMEBUFFER, screenshot_framebuffer.handle); |
| 708 | 464 | ||
| 709 | Layout::FramebufferLayout layout{renderer_settings.screenshot_framebuffer_layout}; | 465 | const Layout::FramebufferLayout layout{renderer_settings.screenshot_framebuffer_layout}; |
| 710 | 466 | ||
| 711 | GLuint renderbuffer; | 467 | GLuint renderbuffer; |
| 712 | glGenRenderbuffers(1, &renderbuffer); | 468 | glGenRenderbuffers(1, &renderbuffer); |
diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 5329577fb..9ef181f95 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h | |||
| @@ -55,14 +55,6 @@ struct ScreenInfo { | |||
| 55 | TextureInfo texture; | 55 | TextureInfo texture; |
| 56 | }; | 56 | }; |
| 57 | 57 | ||
| 58 | struct PresentationTexture { | ||
| 59 | u32 width = 0; | ||
| 60 | u32 height = 0; | ||
| 61 | OGLTexture texture; | ||
| 62 | }; | ||
| 63 | |||
| 64 | class FrameMailbox; | ||
| 65 | |||
| 66 | class RendererOpenGL final : public VideoCore::RendererBase { | 58 | class RendererOpenGL final : public VideoCore::RendererBase { |
| 67 | public: | 59 | public: |
| 68 | explicit RendererOpenGL(Core::TelemetrySession& telemetry_session, | 60 | explicit RendererOpenGL(Core::TelemetrySession& telemetry_session, |
| @@ -74,7 +66,6 @@ public: | |||
| 74 | bool Init() override; | 66 | bool Init() override; |
| 75 | void ShutDown() override; | 67 | void ShutDown() override; |
| 76 | void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) override; | 68 | void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) override; |
| 77 | bool TryPresent(int timeout_ms) override; | ||
| 78 | 69 | ||
| 79 | private: | 70 | private: |
| 80 | /// Initializes the OpenGL state and creates persistent objects. | 71 | /// Initializes the OpenGL state and creates persistent objects. |
| @@ -102,8 +93,6 @@ private: | |||
| 102 | 93 | ||
| 103 | void PrepareRendertarget(const Tegra::FramebufferConfig* framebuffer); | 94 | void PrepareRendertarget(const Tegra::FramebufferConfig* framebuffer); |
| 104 | 95 | ||
| 105 | bool Present(int timeout_ms); | ||
| 106 | |||
| 107 | Core::TelemetrySession& telemetry_session; | 96 | Core::TelemetrySession& telemetry_session; |
| 108 | Core::Frontend::EmuWindow& emu_window; | 97 | Core::Frontend::EmuWindow& emu_window; |
| 109 | Core::Memory::Memory& cpu_memory; | 98 | Core::Memory::Memory& cpu_memory; |
| @@ -134,11 +123,6 @@ private: | |||
| 134 | /// Used for transforming the framebuffer orientation | 123 | /// Used for transforming the framebuffer orientation |
| 135 | Tegra::FramebufferConfig::TransformFlags framebuffer_transform_flags{}; | 124 | Tegra::FramebufferConfig::TransformFlags framebuffer_transform_flags{}; |
| 136 | Common::Rectangle<int> framebuffer_crop_rect; | 125 | Common::Rectangle<int> framebuffer_crop_rect; |
| 137 | |||
| 138 | /// Frame presentation mailbox | ||
| 139 | std::unique_ptr<FrameMailbox> frame_mailbox; | ||
| 140 | |||
| 141 | bool has_debug_tool = false; | ||
| 142 | }; | 126 | }; |
| 143 | 127 | ||
| 144 | } // namespace OpenGL | 128 | } // namespace OpenGL |
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 0e4583986..d38e797a4 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp | |||
| @@ -283,11 +283,6 @@ void RendererVulkan::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) { | |||
| 283 | render_window.PollEvents(); | 283 | render_window.PollEvents(); |
| 284 | } | 284 | } |
| 285 | 285 | ||
| 286 | bool RendererVulkan::TryPresent(int /*timeout_ms*/) { | ||
| 287 | // TODO (bunnei): ImplementMe | ||
| 288 | return true; | ||
| 289 | } | ||
| 290 | |||
| 291 | bool RendererVulkan::Init() { | 286 | bool RendererVulkan::Init() { |
| 292 | library = OpenVulkanLibrary(); | 287 | library = OpenVulkanLibrary(); |
| 293 | instance = CreateInstance(library, dld, render_window.GetWindowInfo().type, | 288 | instance = CreateInstance(library, dld, render_window.GetWindowInfo().type, |
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index ddff77942..5085310d0 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h | |||
| @@ -55,7 +55,6 @@ public: | |||
| 55 | bool Init() override; | 55 | bool Init() override; |
| 56 | void ShutDown() override; | 56 | void ShutDown() override; |
| 57 | void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) override; | 57 | void SwapBuffers(const Tegra::FramebufferConfig* framebuffer) override; |
| 58 | bool TryPresent(int timeout_ms) override; | ||
| 59 | 58 | ||
| 60 | static std::vector<std::string> EnumerateDevices(); | 59 | static std::vector<std::string> EnumerateDevices(); |
| 61 | 60 | ||
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index caa2d06d3..408eac2b7 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp | |||
| @@ -218,15 +218,6 @@ public: | |||
| 218 | 218 | ||
| 219 | virtual ~RenderWidget() = default; | 219 | virtual ~RenderWidget() = default; |
| 220 | 220 | ||
| 221 | /// Called on the UI thread when this Widget is ready to draw | ||
| 222 | /// Dervied classes can override this to draw the latest frame. | ||
| 223 | virtual void Present() {} | ||
| 224 | |||
| 225 | void paintEvent(QPaintEvent* event) override { | ||
| 226 | Present(); | ||
| 227 | update(); | ||
| 228 | } | ||
| 229 | |||
| 230 | QPaintEngine* paintEngine() const override { | 221 | QPaintEngine* paintEngine() const override { |
| 231 | return nullptr; | 222 | return nullptr; |
| 232 | } | 223 | } |
| @@ -245,20 +236,8 @@ public: | |||
| 245 | context = std::move(context_); | 236 | context = std::move(context_); |
| 246 | } | 237 | } |
| 247 | 238 | ||
| 248 | void Present() override { | ||
| 249 | if (!isVisible()) { | ||
| 250 | return; | ||
| 251 | } | ||
| 252 | |||
| 253 | context->MakeCurrent(); | ||
| 254 | if (Core::System::GetInstance().Renderer().TryPresent(100)) { | ||
| 255 | context->SwapBuffers(); | ||
| 256 | glFinish(); | ||
| 257 | } | ||
| 258 | } | ||
| 259 | |||
| 260 | private: | 239 | private: |
| 261 | std::unique_ptr<Core::Frontend::GraphicsContext> context{}; | 240 | std::unique_ptr<Core::Frontend::GraphicsContext> context; |
| 262 | }; | 241 | }; |
| 263 | 242 | ||
| 264 | #ifdef HAS_VULKAN | 243 | #ifdef HAS_VULKAN |
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp index a804d5185..521209622 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp | |||
| @@ -13,9 +13,8 @@ | |||
| 13 | #include "input_common/sdl/sdl.h" | 13 | #include "input_common/sdl/sdl.h" |
| 14 | #include "yuzu_cmd/emu_window/emu_window_sdl2.h" | 14 | #include "yuzu_cmd/emu_window/emu_window_sdl2.h" |
| 15 | 15 | ||
| 16 | EmuWindow_SDL2::EmuWindow_SDL2(Core::System& system, bool fullscreen, | 16 | EmuWindow_SDL2::EmuWindow_SDL2(InputCommon::InputSubsystem* input_subsystem_) |
| 17 | InputCommon::InputSubsystem* input_subsystem_) | 17 | : input_subsystem{input_subsystem_} { |
| 18 | : system{system}, input_subsystem{input_subsystem_} { | ||
| 19 | if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0) { | 18 | if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0) { |
| 20 | LOG_CRITICAL(Frontend, "Failed to initialize SDL2! Exiting..."); | 19 | LOG_CRITICAL(Frontend, "Failed to initialize SDL2! Exiting..."); |
| 21 | exit(1); | 20 | exit(1); |
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.h b/src/yuzu_cmd/emu_window/emu_window_sdl2.h index 82750ffec..53d756c3c 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.h | |||
| @@ -20,8 +20,7 @@ class InputSubsystem; | |||
| 20 | 20 | ||
| 21 | class EmuWindow_SDL2 : public Core::Frontend::EmuWindow { | 21 | class EmuWindow_SDL2 : public Core::Frontend::EmuWindow { |
| 22 | public: | 22 | public: |
| 23 | explicit EmuWindow_SDL2(Core::System& system, bool fullscreen, | 23 | explicit EmuWindow_SDL2(InputCommon::InputSubsystem* input_subsystem); |
| 24 | InputCommon::InputSubsystem* input_subsystem); | ||
| 25 | ~EmuWindow_SDL2(); | 24 | ~EmuWindow_SDL2(); |
| 26 | 25 | ||
| 27 | /// Polls window events | 26 | /// Polls window events |
| @@ -33,9 +32,6 @@ public: | |||
| 33 | /// Returns if window is shown (not minimized) | 32 | /// Returns if window is shown (not minimized) |
| 34 | bool IsShown() const override; | 33 | bool IsShown() const override; |
| 35 | 34 | ||
| 36 | /// Presents the next frame | ||
| 37 | virtual void Present() = 0; | ||
| 38 | |||
| 39 | protected: | 35 | protected: |
| 40 | /// Called by PollEvents when a key is pressed or released. | 36 | /// Called by PollEvents when a key is pressed or released. |
| 41 | void OnKeyEvent(int key, u8 state); | 37 | void OnKeyEvent(int key, u8 state); |
| @@ -67,9 +63,6 @@ protected: | |||
| 67 | /// Called when a configuration change affects the minimal size of the window | 63 | /// Called when a configuration change affects the minimal size of the window |
| 68 | void OnMinimalClientAreaChangeRequest(std::pair<unsigned, unsigned> minimal_size) override; | 64 | void OnMinimalClientAreaChangeRequest(std::pair<unsigned, unsigned> minimal_size) override; |
| 69 | 65 | ||
| 70 | /// Instance of the system, used to access renderer for the presentation thread | ||
| 71 | Core::System& system; | ||
| 72 | |||
| 73 | /// Is the window still open? | 66 | /// Is the window still open? |
| 74 | bool is_open = true; | 67 | bool is_open = true; |
| 75 | 68 | ||
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp index 881b67a76..5f35233b5 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp | |||
| @@ -87,9 +87,8 @@ bool EmuWindow_SDL2_GL::SupportsRequiredGLExtensions() { | |||
| 87 | return unsupported_ext.empty(); | 87 | return unsupported_ext.empty(); |
| 88 | } | 88 | } |
| 89 | 89 | ||
| 90 | EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(Core::System& system, bool fullscreen, | 90 | EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(InputCommon::InputSubsystem* input_subsystem, bool fullscreen) |
| 91 | InputCommon::InputSubsystem* input_subsystem) | 91 | : EmuWindow_SDL2{input_subsystem} { |
| 92 | : EmuWindow_SDL2{system, fullscreen, input_subsystem} { | ||
| 93 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); | 92 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); |
| 94 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); | 93 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); |
| 95 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY); | 94 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY); |
| @@ -163,13 +162,3 @@ EmuWindow_SDL2_GL::~EmuWindow_SDL2_GL() { | |||
| 163 | std::unique_ptr<Core::Frontend::GraphicsContext> EmuWindow_SDL2_GL::CreateSharedContext() const { | 162 | std::unique_ptr<Core::Frontend::GraphicsContext> EmuWindow_SDL2_GL::CreateSharedContext() const { |
| 164 | return std::make_unique<SDLGLContext>(); | 163 | return std::make_unique<SDLGLContext>(); |
| 165 | } | 164 | } |
| 166 | |||
| 167 | void EmuWindow_SDL2_GL::Present() { | ||
| 168 | SDL_GL_MakeCurrent(render_window, window_context); | ||
| 169 | SDL_GL_SetSwapInterval(Settings::values.use_vsync.GetValue() ? 1 : 0); | ||
| 170 | while (IsOpen()) { | ||
| 171 | system.Renderer().TryPresent(100); | ||
| 172 | SDL_GL_SwapWindow(render_window); | ||
| 173 | } | ||
| 174 | SDL_GL_MakeCurrent(render_window, nullptr); | ||
| 175 | } | ||
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h index 732a64edd..dba5c293c 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h | |||
| @@ -14,12 +14,9 @@ class InputSubsystem; | |||
| 14 | 14 | ||
| 15 | class EmuWindow_SDL2_GL final : public EmuWindow_SDL2 { | 15 | class EmuWindow_SDL2_GL final : public EmuWindow_SDL2 { |
| 16 | public: | 16 | public: |
| 17 | explicit EmuWindow_SDL2_GL(Core::System& system, bool fullscreen, | 17 | explicit EmuWindow_SDL2_GL(InputCommon::InputSubsystem* input_subsystem, bool fullscreen); |
| 18 | InputCommon::InputSubsystem* input_subsystem); | ||
| 19 | ~EmuWindow_SDL2_GL(); | 18 | ~EmuWindow_SDL2_GL(); |
| 20 | 19 | ||
| 21 | void Present() override; | ||
| 22 | |||
| 23 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override; | 20 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override; |
| 24 | 21 | ||
| 25 | private: | 22 | private: |
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp index 53491f86e..3ba657c00 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp | |||
| @@ -19,9 +19,8 @@ | |||
| 19 | #include <SDL.h> | 19 | #include <SDL.h> |
| 20 | #include <SDL_syswm.h> | 20 | #include <SDL_syswm.h> |
| 21 | 21 | ||
| 22 | EmuWindow_SDL2_VK::EmuWindow_SDL2_VK(Core::System& system, bool fullscreen, | 22 | EmuWindow_SDL2_VK::EmuWindow_SDL2_VK(InputCommon::InputSubsystem* input_subsystem) |
| 23 | InputCommon::InputSubsystem* input_subsystem) | 23 | : EmuWindow_SDL2{input_subsystem} { |
| 24 | : EmuWindow_SDL2{system, fullscreen, input_subsystem} { | ||
| 25 | const std::string window_title = fmt::format("yuzu {} | {}-{} (Vulkan)", Common::g_build_name, | 24 | const std::string window_title = fmt::format("yuzu {} | {}-{} (Vulkan)", Common::g_build_name, |
| 26 | Common::g_scm_branch, Common::g_scm_desc); | 25 | Common::g_scm_branch, Common::g_scm_desc); |
| 27 | render_window = | 26 | render_window = |
| @@ -74,7 +73,3 @@ EmuWindow_SDL2_VK::~EmuWindow_SDL2_VK() = default; | |||
| 74 | std::unique_ptr<Core::Frontend::GraphicsContext> EmuWindow_SDL2_VK::CreateSharedContext() const { | 73 | std::unique_ptr<Core::Frontend::GraphicsContext> EmuWindow_SDL2_VK::CreateSharedContext() const { |
| 75 | return std::make_unique<DummyContext>(); | 74 | return std::make_unique<DummyContext>(); |
| 76 | } | 75 | } |
| 77 | |||
| 78 | void EmuWindow_SDL2_VK::Present() { | ||
| 79 | // TODO (bunnei): ImplementMe | ||
| 80 | } | ||
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h index f99704d4c..bdfdc3c6f 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h | |||
| @@ -19,11 +19,8 @@ class InputSubsystem; | |||
| 19 | 19 | ||
| 20 | class EmuWindow_SDL2_VK final : public EmuWindow_SDL2 { | 20 | class EmuWindow_SDL2_VK final : public EmuWindow_SDL2 { |
| 21 | public: | 21 | public: |
| 22 | explicit EmuWindow_SDL2_VK(Core::System& system, bool fullscreen, | 22 | explicit EmuWindow_SDL2_VK(InputCommon::InputSubsystem* input_subsystem); |
| 23 | InputCommon::InputSubsystem* input_subsystem); | 23 | ~EmuWindow_SDL2_VK() override; |
| 24 | ~EmuWindow_SDL2_VK(); | ||
| 25 | |||
| 26 | void Present() override; | ||
| 27 | 24 | ||
| 28 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override; | 25 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override; |
| 29 | }; | 26 | }; |
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index e960b5413..3a76c785f 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp | |||
| @@ -185,11 +185,11 @@ int main(int argc, char** argv) { | |||
| 185 | std::unique_ptr<EmuWindow_SDL2> emu_window; | 185 | std::unique_ptr<EmuWindow_SDL2> emu_window; |
| 186 | switch (Settings::values.renderer_backend.GetValue()) { | 186 | switch (Settings::values.renderer_backend.GetValue()) { |
| 187 | case Settings::RendererBackend::OpenGL: | 187 | case Settings::RendererBackend::OpenGL: |
| 188 | emu_window = std::make_unique<EmuWindow_SDL2_GL>(system, fullscreen, &input_subsystem); | 188 | emu_window = std::make_unique<EmuWindow_SDL2_GL>(&input_subsystem, fullscreen); |
| 189 | break; | 189 | break; |
| 190 | case Settings::RendererBackend::Vulkan: | 190 | case Settings::RendererBackend::Vulkan: |
| 191 | #ifdef HAS_VULKAN | 191 | #ifdef HAS_VULKAN |
| 192 | emu_window = std::make_unique<EmuWindow_SDL2_VK>(system, fullscreen, &input_subsystem); | 192 | emu_window = std::make_unique<EmuWindow_SDL2_VK>(&input_subsystem); |
| 193 | break; | 193 | break; |
| 194 | #else | 194 | #else |
| 195 | LOG_CRITICAL(Frontend, "Vulkan backend has not been compiled!"); | 195 | LOG_CRITICAL(Frontend, "Vulkan backend has not been compiled!"); |
| @@ -240,14 +240,11 @@ int main(int argc, char** argv) { | |||
| 240 | system.CurrentProcess()->GetTitleID(), false, | 240 | system.CurrentProcess()->GetTitleID(), false, |
| 241 | [](VideoCore::LoadCallbackStage, size_t value, size_t total) {}); | 241 | [](VideoCore::LoadCallbackStage, size_t value, size_t total) {}); |
| 242 | 242 | ||
| 243 | std::thread render_thread([&emu_window] { emu_window->Present(); }); | ||
| 244 | system.Run(); | 243 | system.Run(); |
| 245 | while (emu_window->IsOpen()) { | 244 | while (emu_window->IsOpen()) { |
| 246 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); | 245 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
| 247 | } | 246 | } |
| 248 | system.Pause(); | 247 | system.Pause(); |
| 249 | render_thread.join(); | ||
| 250 | |||
| 251 | system.Shutdown(); | 248 | system.Shutdown(); |
| 252 | 249 | ||
| 253 | detached_tasks.WaitForAllTasks(); | 250 | detached_tasks.WaitForAllTasks(); |