KiCad PCB EDA Suite
Loading...
Searching...
No Matches
render_thread.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include <rhi/render_thread.h>
21#include <rhi/rhi_device.h>
22#include <rhi/rhi_glyph_atlas.h>
23#include <rhi/rhi_vertex.h>
24#include <rhi/rhi_buffer.h>
26#include <rhi/rhi_supersample.h>
27#include <rhi/layer_draw_plan.h>
28#include <rhi/rhi_cull_runs.h>
29
30#include <advanced_config.h>
31#include <core/profile.h>
32#include <kiplatform/app.h>
33#include <trace_helpers.h>
34
35#include <wx/image.h>
36#include <wx/log.h>
37
38#include <algorithm>
39#include <cmath>
40#include <cstdio>
41#include <cstdlib>
42#include <cstring>
43#include <future>
44#include <string>
45#include <vector>
46
47namespace
48{
49// Raising priority may need privileges, so a failure is only logged.
50void ApplyRenderThreadPriority( const wxString& aPriority )
51{
52 const bool low = aPriority.CmpNoCase( wxS( "low" ) ) == 0;
53 const bool high = aPriority.CmpNoCase( wxS( "high" ) ) == 0;
54
55 if( !low && !high )
56 return; // "normal" or unrecognized leaves default scheduling
57
59 wxLogTrace( traceRhi, wxS( "RenderThread: could not set thread priority '%s'" ), aPriority );
60}
61} // namespace
62
63
64static long long WindowStateAgeMs( KIGFX::WINDOW_STATE::Clock::time_point aThen,
65 KIGFX::WINDOW_STATE::Clock::time_point aNow )
66{
67 if( aThen.time_since_epoch().count() == 0 )
68 return -1;
69
70 return static_cast<long long>(
71 std::chrono::duration_cast<std::chrono::milliseconds>( aNow - aThen ).count() );
72}
73
74
75static double WindowStateDurationMs( KIGFX::WINDOW_STATE::Clock::time_point aStart,
76 KIGFX::WINDOW_STATE::Clock::time_point aEnd )
77{
78 if( aStart.time_since_epoch().count() == 0 || aEnd.time_since_epoch().count() == 0 )
79 return 0.0;
80
81 return std::chrono::duration<double, std::milli>( aEnd - aStart ).count();
82}
83
84
85// Depth is for the layer slices
87{
89 desc.width = aWS.RenderWidth();
90 desc.height = aWS.RenderHeight();
92 desc.hasDepthStencil = true;
93 return desc;
94}
95
96
98{
99 aDevice.Release( aWS.diffBelowFBO );
100 aDevice.Release( aWS.staticDiffBelowFBO );
101}
102
103
104static uint32_t staticMarginFor( const KIGFX::WINDOW_STATE& aWS, uint32_t aMaxTextureSize )
105{
106 if( aWS.supersampleFactor == 0 )
107 return 0;
108
109 const uint32_t renderSize = std::max( aWS.RenderWidth(), aWS.RenderHeight() );
110 const uint32_t room = renderSize < aMaxTextureSize
111 ? ( aMaxTextureSize - renderSize ) / ( 2 * aWS.supersampleFactor )
112 : 0;
113
114 // A quarter of the view lets a drag pan for several frames per redraw. The caps bound a
115 // supersampled target's size and keep it within the device's texture limit.
116 return std::min( { std::max( aWS.backingWidth, aWS.backingHeight ) / 4, 512u / aWS.supersampleFactor, room } );
117}
118
119
120// The largest factor up to aRequested, from 1 to 4, whose layer targets fit the device's texture size
121static uint32_t fitSupersampleFactor( uint32_t aRequested, const KIGFX::WINDOW_STATE& aWS,
122 uint32_t aMaxTextureSize )
123{
124 uint32_t factor = std::clamp<uint32_t>( aRequested, 1, 4 );
125
126 while( factor > 1 && std::max( aWS.backingWidth, aWS.backingHeight ) * factor > aMaxTextureSize )
127 --factor;
128
129 return factor;
130}
131
132
133// How a target with a margin serves the current view, see WINDOW_STATE::staticMargin
135{
136 bool reuse = false; // show what the target holds, shifted by shift
138 uint32_t drawMargin = 0; // how much of the margin a redraw covers
139};
140
141
143 const KIGFX::FRAME_DRAW_DESC& aNow, uint32_t aMargin, bool aDrawsGrid )
144{
146
147 const bool sameGrid = aDrawn.drawGrid == aNow.drawGrid
148 && ( !aNow.drawGrid || KIGFX::RhiGridMovedOnly( aDrawn.gridUBO, aNow.gridUBO ) );
149 const bool sameContent = aDrawn.valid && aDrawn.epoch == aEpoch && ( !aDrawsGrid || sameGrid );
150
151 if( !sameContent )
152 return plan;
153
154 const std::optional<KIGFX::RHI_VIEW_SHIFT> shift = KIGFX::RhiViewShift( aDrawn.frameUBO, aNow.frameUBO );
155
156 if( shift && std::abs( shift->x ) <= static_cast<int>( aDrawn.margin )
157 && std::abs( shift->y ) <= static_cast<int>( aDrawn.margin ) )
158 {
159 plan.reuse = true;
160 plan.shift = *shift;
161 }
162 else if( KIGFX::RhiViewMovedOnly( aDrawn.frameUBO, aNow.frameUBO ) )
163 {
164 // A zoom draws only the view, a pan its margin too
165 plan.drawMargin = aMargin;
166 }
167
168 return plan;
169}
170
171
172// Sample only the part of a margin target the view sits in
174 const KIGFX::RHI_VIEW_SHIFT& aShift )
175{
176 COMPOSITOR_LAYER layer{ aFBO, 1.0f };
177
178 if( aWS.staticMargin > 0 )
179 {
180 const float ss = static_cast<float>( aWS.supersampleFactor );
181 const float w = static_cast<float>( aWS.StaticWidth() );
182 const float h = static_cast<float>( aWS.StaticHeight() );
183
184 layer.uvScale[0] = aWS.RenderWidth() / w;
185 layer.uvScale[1] = aWS.RenderHeight() / h;
186 layer.uvOffset[0] = ( static_cast<float>( aWS.staticMargin ) - aShift.x ) * ss / w;
187 layer.uvOffset[1] = ( static_cast<float>( aWS.staticMargin ) - aShift.y ) * ss / h;
188 }
189
190 return layer;
191}
192
193
195{
197 desc.width = aWS.StaticWidth();
198 desc.height = aWS.StaticHeight();
199 return desc;
200}
201
202
204 KIGFX::WINDOW_STATE::Clock::time_point aNow,
205 std::chrono::milliseconds aBackoff =
206 std::chrono::milliseconds( 250 ) )
207{
208 if( aWS.lastSwapchainCreateAttemptTime.time_since_epoch().count() == 0 )
209 return true;
210
211 return std::chrono::duration_cast<std::chrono::milliseconds>(
212 aNow - aWS.lastSwapchainCreateAttemptTime ) >= aBackoff;
213}
214
215
216namespace KIGFX
217{
218
219
221{
222 // Not resized in place because a failed resize leaves a non-null but empty swapchain that
223 // Present() dereferences.
224 m_device->Release( aWS.swapchain );
225
226 // A swapchain must be destroyed before its surface
227 if( aSurfaceLost )
228 m_device->Release( aWS.surface );
229
230 EnsureSwapchain( aWS, "present-out-of-date" );
231}
232
233
234bool RENDER_THREAD::EnsureSwapchain( WINDOW_STATE& aWS, const char* aReason )
235{
236 if( !m_device || aWS.swapchain
237 || aWS.backingWidth == 0 || aWS.backingHeight == 0 )
238 {
239 return static_cast<bool>( aWS.swapchain );
240 }
241
242 auto now = WINDOW_STATE::Clock::now();
243
244 if( !ShouldRetrySwapchainCreate( aWS, now ) )
245 return false;
246
247 // GTK/X11 can hand over a child window before it is mapped, and a VkSurfaceKHR made that early
248 // never recovers, so a retry remakes the surface too.
249 if( aWS.swapchainCreateAttempts > 0 || !aWS.surface )
250 {
251 if( aWS.surface )
252 {
253 wxLogTrace( traceRhi, wxS( "RenderThread: refreshing surface before "
254 "swapchain retry windowId=%u reason=%s "
255 "attempt=%llu native=%p" ),
256 aWS.windowId, aReason,
257 static_cast<unsigned long long>( aWS.swapchainCreateAttempts ) + 1,
259 m_device->DestroySurface( aWS.surface );
260 aWS.surface = {};
261 }
262
263 if( aWS.nativeHandle.nativeHandle )
264 {
265 RHI_SURFACE_DESC surfDesc;
266 surfDesc.window = aWS.nativeHandle;
267 aWS.surface = m_device->CreateSurface( surfDesc );
268 }
269
270 wxLogTrace( traceRhi, wxS( "RenderThread: refreshed surface windowId=%u -> %s" ),
271 aWS.windowId, aWS.surface ? "OK" : "FAILED" );
272 }
273
274 if( !aWS.surface )
275 {
278 return false;
279 }
280
283
284 RHI_SWAPCHAIN_DESC scDesc;
285 scDesc.surface = aWS.surface;
286 scDesc.width = aWS.backingWidth;
287 scDesc.height = aWS.backingHeight;
288 scDesc.dpiScale = aWS.dpiScale;
289 aWS.swapchain = m_device->CreateSwapchain( scDesc );
290
291 wxLogTrace( traceRhi, wxS( "RenderThread: EnsureSwapchain reason=%s windowId=%u "
292 "attempt=%llu %ux%u -> %s" ),
293 aReason, aWS.windowId,
294 static_cast<unsigned long long>( aWS.swapchainCreateAttempts ),
296 aWS.swapchain ? "OK" : "FAILED" );
297
298 if( aWS.swapchain )
299 {
300 // The window is not activated here so that a new window presents nothing until a frame
301 // is requested.
302 aWS.needsStaticInvalidate = true;
303 }
304
305 return static_cast<bool>( aWS.swapchain );
306}
307
308
310 m_deviceFactory( std::move( aDeviceFactory ) )
311{
312}
313
314
319
320
322{
323 if( m_running.load( std::memory_order_acquire ) )
324 return true;
325
326 m_shutdownRequested.store( false, std::memory_order_release );
327
328 std::promise<bool> started;
329 std::future<bool> initialized = started.get_future();
330
331 m_thread = std::thread( &RENDER_THREAD::ThreadMain, this, std::move( started ) );
332
333 if( !initialized.get() )
334 {
335 if( m_thread.joinable() )
336 m_thread.join();
337
338 return false;
339 }
340
341 return true;
342}
343
344
346{
347 return m_running.load( std::memory_order_acquire );
348}
349
350
352{
353 // Taking the mutex keeps a notify from landing between the waiter's predicate check and its
354 // wait, where it would be lost until the idle heartbeat.
355 {
356 std::lock_guard<std::mutex> lock( m_workMutex );
357 }
358
359 m_workCV.notify_one();
360}
361
362
364{
365 {
366 // Pushing under the lock keeps a command from landing after the shutdown drain
367 std::lock_guard<std::mutex> lock( m_workMutex );
368
369 if( !m_acceptingWork )
370 return false;
371
372 m_controlQueue.move_push( std::move( aCmd ) );
373 }
374
375 m_workCV.notify_one();
376 return true;
377}
378
379
381{
382 {
383 std::lock_guard<std::mutex> lock( m_workMutex );
384
385 if( !m_acceptingWork )
386 return;
387
388 m_uploadQueue.move_push( std::move( aCmd ) );
389 m_uploadsPending.store( true, std::memory_order_release );
390 }
391
392 m_workCV.notify_one();
393}
394
395
397{
398 std::lock_guard<std::mutex> lock( m_deviceMutex );
399 return m_device ? m_device->ReserveBufferHandle() : RHI_BufferHandle{};
400}
401
402
404{
405 std::lock_guard<std::mutex> lock( m_deviceMutex );
406 return m_device ? m_device->ReserveTextureHandle() : RHI_TextureHandle{};
407}
408
409
410bool RENDER_THREAD::WaitForFence( std::chrono::milliseconds aTimeout )
411{
412 return runFenced( {}, aTimeout );
413}
414
415
416bool RENDER_THREAD::runFenced( std::function<void()> aWork, std::chrono::milliseconds aTimeout )
417{
418 // Shared so the render thread can still fire the fence after this call has timed out
419 auto done = std::make_shared<std::promise<void>>();
420 std::future<void> completed = done->get_future();
421
422 RHI_CMD_FENCE fence;
423 fence.flushFrames = static_cast<bool>( aWork );
424 fence.onComplete = [done, work = std::move( aWork )]()
425 {
426 if( work )
427 work();
428
429 done->set_value();
430 };
431
432 if( !SubmitControl( std::move( fence ) ) )
433 return false;
434
435 return completed.wait_for( aTimeout ) == std::future_status::ready;
436}
437
438
439bool RENDER_THREAD::ReadbackWindowPixels( RHI_WINDOW_ID aWindowId, wxImage& aImage )
440{
441 struct READBACK
442 {
443 std::vector<uint8_t> pixels; // empty when the readback failed
444 uint32_t width = 0;
445 uint32_t height = 0;
446 uint32_t backingWidth = 0;
447 uint32_t backingHeight = 0;
448 };
449
450 // Shared so the render thread can still write it after this call has timed out
451 auto out = std::make_shared<READBACK>();
452
453 auto readback = [this, aWindowId, out]()
454 {
455 auto it = m_windows.find( aWindowId );
456
457 if( it == m_windows.end() || !it->second.staticFBO || !m_device || IsDeviceLost() )
458 return;
459
460 const WINDOW_STATE& ws = it->second;
461
462 // Targets are supersampled but screenshots are at backing resolution
463 const uint32_t rw = ws.RenderWidth();
464 const uint32_t rh = ws.RenderHeight();
465
466 // Without the overlay and cursor, matching what the OpenGL canvas reads from its main buffer
467 std::vector<COMPOSITOR_LAYER> layers;
468
469 if( ws.staticInMargin )
470 layers.push_back( marginLayer( ws, ws.staticFBO, ws.staticShift ) );
471 else
472 layers.push_back( { ws.ShownStaticFBO(), 1.0f } );
473
474 if( ws.dynamicFBO && ws.dynamicGeometryDrawn )
475 layers.push_back( { ws.dynamicFBO, 1.0f } );
476
477 RHI_FRAMEBUFFER_DESC pictureDesc;
478 pictureDesc.width = rw;
479 pictureDesc.height = rh;
480 pictureDesc.colorFormats.push_back( RHI_TEXTURE_FORMAT::RGBA8 );
481
482 const RHI_FramebufferHandle picture = m_compositor && m_compositor->IsInitialized()
483 ? m_device->CreateFramebuffer( pictureDesc )
485
486 std::vector<uint8_t> renderPixels( static_cast<size_t>( rw ) * rh * 4 );
487
488 const bool read = picture
489 && m_compositor->Composite( picture, rw, rh, layers, ws.drawDesc.backgroundColor )
490 && m_device->ReadbackFramebufferPixels( picture, 0, renderPixels.data(),
491 renderPixels.size() );
492
493 if( picture )
494 m_device->DestroyFramebuffer( picture );
495
496 if( !read )
497 return;
498
499 out->width = rw;
500 out->height = rh;
501 out->backingWidth = ws.backingWidth;
502 out->backingHeight = ws.backingHeight;
503 out->pixels = std::move( renderPixels );
504 };
505
506 if( !runFenced( std::move( readback ), std::chrono::seconds( 5 ) ) || out->pixels.empty() )
507 return false;
508
509 const size_t pixelCount = static_cast<size_t>( out->width ) * out->height;
510
511 if( !aImage.Create( static_cast<int>( out->width ), static_cast<int>( out->height ), false ) )
512 return false;
513
514 unsigned char* rgb = aImage.GetData();
515
516 // The readback is RGBA8 with a top-left origin and the screenshot is opaque RGB
517 for( size_t i = 0; i < pixelCount; ++i )
518 std::memcpy( rgb + i * 3, out->pixels.data() + i * 4, 3 );
519
520 if( out->backingWidth != out->width || out->backingHeight != out->height )
521 {
522 aImage.Rescale( static_cast<int>( out->backingWidth ), static_cast<int>( out->backingHeight ),
523 wxIMAGE_QUALITY_BOX_AVERAGE );
524 }
525
526 return aImage.IsOk();
527}
528
529
531{
532 wxLogTrace( traceRhi, wxS( "RenderThread: Shutdown requested (running=%s)" ),
533 m_running.load( std::memory_order_acquire ) ? "yes" : "no" );
534
535 if( !m_running.load( std::memory_order_acquire ) )
536 {
537 if( m_thread.joinable() )
538 m_thread.join();
539
540 return;
541 }
542
543 m_shutdownRequested.store( true, std::memory_order_release );
545
546 wxLogTrace( traceRhi, wxS( "RenderThread: waiting for thread join" ) );
547
548 if( m_thread.joinable() )
549 m_thread.join();
550
551 wxLogTrace( traceRhi, wxS( "RenderThread: joined" ) );
552}
553
554
555void RENDER_THREAD::ThreadMain( std::promise<bool> aStarted )
556{
557 wxLogTrace( traceRhi, wxS( "RenderThread: starting" ) );
558
559 ApplyRenderThreadPriority( ADVANCED_CFG::GetCfg().m_RenderThreadPriority );
560
561 bool initialized = false;
562
563 // An exception escaping this thread would terminate KiCad instead of letting the canvas fall back
564 try
565 {
566 if( m_deviceFactory )
568
569 const bool deviceReady = m_device && m_device->IsInitialized();
570
571 if( !deviceReady )
572 wxLogTrace( traceRhi, wxS( "RenderThread: device init FAILED" ) );
573
574 // A device without pipelines would draw nothing, so it fails the start and the canvas falls back
575 initialized = deviceReady && InitRenderInfrastructure();
576
577 if( deviceReady && !initialized )
578 wxLogTrace( traceRhi, wxS( "RenderThread: InitRenderInfrastructure FAILED" ) );
579 }
580 catch( const std::exception& e )
581 {
582 wxLogTrace( traceRhi, wxS( "RenderThread: EXCEPTION during init: %s" ), e.what() );
583 }
584 catch( ... )
585 {
586 wxLogTrace( traceRhi, wxS( "RenderThread: UNKNOWN EXCEPTION during init" ) );
587 }
588
589 if( !initialized )
590 {
592
593 {
594 std::lock_guard<std::mutex> lock( m_deviceMutex );
595 m_device.reset();
596 }
597
598 aStarted.set_value( false );
599 return;
600 }
601
602 m_backendType.store( m_device->GetBackend(), std::memory_order_release );
603 m_maxTextureSize.store( m_device->MaxTextureSize(), std::memory_order_release );
604 m_running.store( true, std::memory_order_release );
605
606 {
607 std::lock_guard<std::mutex> lock( m_workMutex );
608 m_acceptingWork = true;
609 }
610
611 wxLogTrace( traceRhi, wxS( "RenderThread: device '%s' initialized OK" ), m_device->GetName() );
612
613 aStarted.set_value( true );
614
615 wxLogTrace( traceRhi, wxS( "RenderThread: entering main loop" ) );
616
617 while( !m_shutdownRequested.load( std::memory_order_acquire ) )
618 {
619 try
620 {
621 m_loopPhase.store( 1, std::memory_order_relaxed );
623
624 if( m_shutdownRequested.load( std::memory_order_acquire ) )
625 break;
626
627 // Control commands still drain above so shutdown and window teardown work on a lost
628 // device. Nothing else does until the canvas replaces this thread.
629 if( !m_device || IsDeviceLost() )
630 {
631 m_uploadQueue.clear();
632 std::this_thread::sleep_for( std::chrono::milliseconds( 8 ) );
633 continue;
634 }
635
636 m_loopPhase.store( 2, std::memory_order_relaxed );
638
639 m_loopPhase.store( 3, std::memory_order_relaxed );
641
642 m_loopPhase.store( 4, std::memory_order_relaxed );
643
644 if( !IsDeviceLost() )
645 m_device->EndFrame();
646
647 m_loopPhase.store( 0, std::memory_order_relaxed );
648
649 // Only deferred releases and a window still waiting for its swapchain need the short
650 // tick. Otherwise a slow heartbeat lets an idle canvas sleep.
651 bool needsTick = m_device && m_device->PendingDeferredReleases() > 0;
652
653 for( const auto& [id, ws] : m_windows )
654 needsTick = needsTick || !ws.swapchain;
655
656 std::chrono::microseconds tick = needsTick ? std::chrono::milliseconds( 16 )
657 : std::chrono::milliseconds( 500 );
658
660 {
661 const auto due = std::chrono::ceil<std::chrono::microseconds>( *m_deferredFrameDue
662 - WINDOW_STATE::Clock::now() );
663
664 tick = std::clamp( due, std::chrono::microseconds( 0 ), tick );
665 }
666
667 {
668 // The predicate catches work queued while this thread was busy above, whose
669 // notify arrived before the wait and was lost.
670 std::unique_lock<std::mutex> lock( m_workMutex );
671 m_workCV.wait_for( lock, tick,
672 [this]()
673 {
674 return m_shutdownRequested.load(
675 std::memory_order_acquire )
676 || !m_controlQueue.empty()
677 || m_uploadsPending.load(
678 std::memory_order_acquire );
679 } );
680 }
681 }
682 catch( const std::exception& e )
683 {
684 wxLogTrace( traceRhi, wxS( "RenderThread: EXCEPTION in main loop phase=%d: %s" ),
685 m_loopPhase.load( std::memory_order_relaxed ), e.what() );
686 }
687 catch( ... )
688 {
689 wxLogTrace( traceRhi, wxS( "RenderThread: UNKNOWN EXCEPTION in main loop "
690 "phase=%d" ),
691 m_loopPhase.load( std::memory_order_relaxed ) );
692 }
693 }
694
695 wxLogTrace( traceRhi, wxS( "RenderThread: shutting down (windows=%zu)" ), m_windows.size() );
696
697 {
698 std::lock_guard<std::mutex> lock( m_workMutex );
699 m_acceptingWork = false;
700 }
701
704
707
708 {
709 std::lock_guard<std::mutex> lock( m_deviceMutex );
710 m_device.reset();
711 }
712
713 m_running.store( false, std::memory_order_release );
714
715 wxLogTrace( traceRhi, wxS( "RenderThread: shutdown complete" ) );
716}
717
718
720{
722
723 while( m_controlQueue.pop( cmd ) )
724 {
725 std::visit(
726 [this]( auto&& arg )
727 {
728 using T = std::decay_t<decltype( arg )>;
729
730 if constexpr( std::is_same_v<T, RHI_CMD_FRAME_REQUEST> )
731 {
732 if( !m_shutdownRequested.load( std::memory_order_acquire ) )
733 HandleFrameRequest( arg );
734 }
735 else if constexpr( std::is_same_v<T, RHI_CMD_SWAPCHAIN_CREATE> )
736 {
737 // A lost device makes nothing new, see OnDeviceLost()
738 if( !IsDeviceLost() )
740 }
741 else if constexpr( std::is_same_v<T, RHI_CMD_SWAPCHAIN_RESIZE> )
742 {
743 if( !IsDeviceLost() )
745 }
746 else if constexpr( std::is_same_v<T, RHI_CMD_SWAPCHAIN_DESTROY> )
747 {
749 }
750 else if constexpr( std::is_same_v<T, RHI_CMD_FENCE> )
751 {
752 HandleFence( arg );
753 }
754 },
755 cmd );
756 }
757}
758
759
761{
762 m_uploadsPending.store( false, std::memory_order_release );
763
764 // Uploads to a lost device have nowhere to go, and the queue must not grow meanwhile
765 if( !m_device || IsDeviceLost() )
766 {
767 m_uploadQueue.clear();
768 return;
769 }
770
771 m_device->BeginUploadBatch();
772
774
775 while( m_uploadQueue.pop( cmd ) )
776 {
777 std::visit(
778 [&]( auto&& arg )
779 {
780 using T = std::decay_t<decltype( arg )>;
781
782 if constexpr( std::is_same_v<T, RHI_GEOMETRY_UPLOAD> )
783 {
784 if( arg.targetBuffer && arg.HasStaging() )
785 {
786 m_device->UpdateBuffer( arg.targetBuffer, arg.stagingData,
787 arg.stagingSize, arg.targetOffset );
788 }
789 }
790 else if constexpr( std::is_same_v<T, RHI_TEXTURE_UPLOAD> )
791 {
792 if( arg.targetTexture && arg.HasStaging() )
793 m_device->UploadTexture( arg.targetTexture, arg.stagingData, arg.stagingSize );
794 }
795 else if constexpr( std::is_same_v<T, RHI_BUFFER_CREATE> )
796 {
797 // Realized before later uploads and draws use it. A failure frees the slot
798 // and leaves the handle invalid.
799 if( arg.handle )
800 m_device->RealizeBuffer( arg.handle, arg.desc );
801 }
802 else if constexpr( std::is_same_v<T, RHI_TEXTURE_CREATE> )
803 {
804 if( arg.handle )
805 m_device->RealizeTexture( arg.handle, arg.desc );
806 }
807 else if constexpr( std::is_same_v<T, RHI_BUFFER_DESTROY> )
808 {
809 m_device->DestroyBuffer( arg.handle );
810 }
811 else if constexpr( std::is_same_v<T, RHI_TEXTURE_DESTROY> )
812 {
813 m_device->DestroyTexture( arg.handle );
814 }
815 },
816 cmd );
817 }
818
819 m_device->EndUploadBatch();
820}
821
822
824{
825 if( !m_device )
826 return false;
827
828 m_pipelines = std::make_unique<RHI_GAL_PIPELINES>( *m_device );
829
830 if( !m_pipelines->Initialize() )
831 {
832 m_pipelines.reset();
833 return false;
834 }
835
836 // Two NDC triangles bound at slot 0 for all instanced geometry draws
837 RHI_VERTEX unitQuad[6];
838
839 auto setVert = []( RHI_VERTEX& v, float px, float py, float tu, float tv )
840 {
841 v.SetPosition( px, py );
842 v.SetColor( 255, 255, 255, 255 );
843 v.SetUV( tu, tv );
844 };
845
846 setVert( unitQuad[0], -1.0f, -1.0f, 0.0f, 0.0f );
847 setVert( unitQuad[1], 1.0f, -1.0f, 1.0f, 0.0f );
848 setVert( unitQuad[2], 1.0f, 1.0f, 1.0f, 1.0f );
849 setVert( unitQuad[3], -1.0f, -1.0f, 0.0f, 0.0f );
850 setVert( unitQuad[4], 1.0f, 1.0f, 1.0f, 1.0f );
851 setVert( unitQuad[5], -1.0f, 1.0f, 0.0f, 1.0f );
852
853 auto makeBuffer = [&]( RHI_BUFFER_TYPE aType, const void* aData, size_t aSize )
854 {
855 RHI_BUFFER_DESC desc;
856 desc.type = aType;
857 desc.sizeBytes = aSize;
858
859 RHI_BufferHandle buffer = m_device->CreateBuffer( desc );
860
861 if( buffer )
862 m_device->UpdateBuffer( buffer, aData, aSize );
863
864 return buffer;
865 };
866
867 // The same two triangles drawn indexed, so each instance shades its 4 corners once
868 const RHI_VERTEX corners[4] = { unitQuad[0], unitQuad[1], unitQuad[2], unitQuad[5] };
869 const uint16_t quadIndices[6] = { 0, 1, 2, 0, 2, 3 };
870
871 m_unitQuadBuffer = makeBuffer( RHI_BUFFER_TYPE::VERTEX, unitQuad, sizeof( unitQuad ) );
872 m_quadCornerBuffer = makeBuffer( RHI_BUFFER_TYPE::VERTEX, corners, sizeof( corners ) );
873 m_quadIndexBuffer = makeBuffer( RHI_BUFFER_TYPE::INDEX, quadIndices, sizeof( quadIndices ) );
874
876 RHI_COLOR_ATTACHMENT_DESC colorAttach;
877 colorAttach.clearColor[3] = 0.0f;
878 rpDesc.colorAttachments.push_back( colorAttach );
879 rpDesc.depthStencilAttachment.enabled = true;
883 m_geometryRenderPass = m_device->CreateRenderPass( rpDesc );
884
885 // Resumes geometry after inline compositing, keeping color and depth
886 RHI_RENDER_PASS_DESC loadRpDesc = rpDesc;
887 loadRpDesc.colorAttachments[0].loadOp = RHI_LOAD_OP::LOAD;
889 m_geometryLoadRenderPass = m_device->CreateRenderPass( loadRpDesc );
890
891 RHI_RENDER_PASS_DESC depthLoadRpDesc = rpDesc;
893 m_geometryDepthLoadRenderPass = m_device->CreateRenderPass( depthLoadRpDesc );
894
895 RHI_RENDER_PASS_DESC discardRpDesc = rpDesc;
897 m_geometryDepthDiscardRenderPass = m_device->CreateRenderPass( discardRpDesc );
898
900 m_geometryDepthLoadDiscardRenderPass = m_device->CreateRenderPass( discardRpDesc );
901
902 // The other passes and buffers have fallbacks, these do not
904 return false;
905
906 // Shared by all windows, each compositing into its own accumulation target
907 m_compositor = std::make_unique<RHI_COMPOSITOR>( *m_device );
908
909 if( !m_compositor->Initialize( m_pipelines->GetTextureSamplerLayout() ) )
910 return false;
911
912 m_compositor->SetCompositorPipeline( m_pipelines->GetCompositorPipeline() );
913
914 return true;
915}
916
917
919{
920 m_compositor.reset();
921
922 if( m_device )
923 {
927 {
928 m_device->Release( *pass );
929 }
930
932 m_device->Release( *buffer );
933 }
934
936 m_pipelines.reset();
937}
938
939
941{
942 if( m_device )
943 {
944 m_device->Release( m_glyphBG );
945 m_device->Release( m_glyphTexture );
946 m_device->Release( m_glyphTable );
947 }
948
949 m_glyphBG = {};
950 m_glyphTexture = {};
951 m_glyphTable = {};
952 m_glyphBGHasAtlas = false;
953}
954
955
956void RENDER_THREAD::SetGlyphAtlas( std::shared_ptr<const RHI_GLYPH_ATLAS_DATA> aAtlas )
957{
958 std::lock_guard<std::mutex> lock( m_glyphAtlasMutex );
959
960 if( !m_glyphAtlas )
961 m_glyphAtlas = std::move( aAtlas );
962}
963
964
966{
967 std::shared_ptr<const RHI_GLYPH_ATLAS_DATA> atlas;
968
969 {
970 std::lock_guard<std::mutex> lock( m_glyphAtlasMutex );
971 atlas = m_glyphAtlas;
972 }
973
974 // Every glyph pipeline needs the set, so it holds a blank atlas until a canvas supplies the font
975 if( m_glyphBG && ( m_glyphBGHasAtlas || !atlas ) )
976 return m_glyphBG;
977
979
980 RHI_TEXTURE_DESC texDesc;
981 texDesc.width = atlas ? atlas->width : 1;
982 texDesc.height = atlas ? atlas->height : 1;
985 m_glyphTexture = m_device->CreateTexture( texDesc );
986
987 const std::vector<uint8_t> blankTexel( 4, 0 );
988 const std::vector<float> blankTable( ( RHI_GLYPH_TABLE_SIZE + 1 ) * 4, 0.0f );
989
990 RHI_BUFFER_DESC bufDesc;
992 bufDesc.sizeBytes = blankTable.size() * sizeof( float );
993 bufDesc.mappable = true;
994 m_glyphTable = m_device->CreateBuffer( bufDesc );
995
997 {
999 return {};
1000 }
1001
1002 const std::vector<uint8_t>& texels = atlas ? atlas->rgba : blankTexel;
1003 const std::vector<float>& table = atlas ? atlas->table : blankTable;
1004
1005 m_device->UploadTexture( m_glyphTexture, texels.data(), texels.size() );
1006 m_device->UpdateBuffer( m_glyphTable, table.data(), std::min( table.size(), blankTable.size() ) * sizeof( float ) );
1007
1008 RHI_BIND_GROUP_DESC bgDesc;
1009 bgDesc.layout = m_pipelines->GetGlyphLayout();
1010 bgDesc.entries.push_back( { 0, {}, m_glyphTexture, 0, 0 } );
1011 bgDesc.entries.push_back(
1012 { RHI_GLYPH_TABLE_BINDING, m_glyphTable, {}, 0, static_cast<uint32_t>( bufDesc.sizeBytes ) } );
1013 m_glyphBG = m_device->CreateBindGroup( bgDesc );
1014 m_glyphBGHasAtlas = static_cast<bool>( atlas );
1015
1016 return m_glyphBG;
1017}
1018
1019
1021{
1022 auto it = m_windows.find( aCmd.windowId );
1023
1024 if( it == m_windows.end() )
1025 {
1026 wxLogTrace( traceRhi, wxS( "RenderThread: FrameRequest windowId=%u UNKNOWN" ),
1027 aCmd.windowId );
1028 return;
1029 }
1030
1031 WINDOW_STATE& ws = it->second;
1033
1034 ws.refreshHz = aCmd.refreshHz;
1035 ws.active = true;
1036 ws.frameResultPending = true;
1037 ws.nextPresentRetryAt = {};
1038 ws.lastActivityTime = WINDOW_STATE::Clock::now();
1040}
1041
1042
1043void RENDER_THREAD::ProcessActiveWindows( bool aIgnorePacing )
1044{
1045 m_activeWindowOrder.clear();
1046 m_deferredFrameDue.reset();
1047
1048 if( IsDeviceLost() )
1049 return;
1050
1051 for( auto& [id, ws] : m_windows )
1052 {
1053 const bool swapchainMissing = !ws.swapchain;
1054
1055 if( swapchainMissing )
1056 EnsureSwapchain( ws, "idle" );
1057
1058 if( swapchainMissing && ws.swapchain && ( ws.frameResultPending || ws.presentSuccessCount > 0 ) )
1059 ws.active = true;
1060
1061 if( ws.swapchain )
1062 {
1063 switch( m_device->PollSurfaceExtent( ws.swapchain ) )
1064 {
1066 {
1067 auto due = WINDOW_STATE::Clock::now() + std::chrono::milliseconds( 50 );
1068 deferFrameUntil( due );
1069 continue;
1070 }
1071
1073 if( ws.frameResultPending || ws.presentSuccessCount > 0 )
1074 {
1075 ws.active = true;
1076 ws.nextPresentRetryAt = {};
1077 }
1078 break;
1079
1082 ws.active = ws.frameResultPending || ws.presentSuccessCount > 0;
1083
1084 if( ws.active )
1085 {
1086 auto due = WINDOW_STATE::Clock::now() + std::chrono::milliseconds( 50 );
1087 deferFrameUntil( due );
1088 }
1089
1090 continue;
1091
1093 break;
1094 }
1095 }
1096
1097 if( !ws.swapchain && ( ws.frameResultPending || ws.presentSuccessCount > 0 ) )
1098 {
1099 auto due = WINDOW_STATE::Clock::now() + std::chrono::milliseconds( 50 );
1100 deferFrameUntil( due );
1101 continue;
1102 }
1103
1104 if( ws.nextPresentRetryAt.time_since_epoch().count() != 0 )
1105 {
1106 if( WINDOW_STATE::Clock::now() < ws.nextPresentRetryAt )
1107 {
1108 deferFrameUntil( ws.nextPresentRetryAt );
1109 continue;
1110 }
1111
1112 ws.nextPresentRetryAt = {};
1113 ws.active = ws.frameResultPending;
1114 }
1115
1116 if( ws.active )
1117 m_activeWindowOrder.push_back( &ws );
1118 }
1119
1120 if( m_activeWindowOrder.empty() )
1121 return;
1122
1123 std::sort( m_activeWindowOrder.begin(), m_activeWindowOrder.end(),
1124 []( const WINDOW_STATE* a, const WINDOW_STATE* b )
1125 {
1126 return a->lastActivityTime > b->lastActivityTime;
1127 } );
1128
1130 {
1131 // An earlier window may have just lost the device
1132 if( IsDeviceLost() )
1133 return;
1134
1135 FRAME_MAILBOX* frameMailbox = ws->mailbox;
1136
1137 // A present that does not wait for the display would draw unseen frames, so such a window is held to one
1138 // per refresh. New input is never held back, and a MAILBOX swapchain drops unseen frames itself.
1139 const bool newInput = frameMailbox && frameMailbox->PendingInputSerial() > ws->drawnInputSerial;
1140 const bool capped = ws->swapchain && ws->refreshHz > 0.0f
1141 && !m_device->PresentWaitsForDisplay( ws->swapchain )
1142 && !m_device->PresentReplacesQueued( ws->swapchain );
1143 const WINDOW_STATE::Clock::time_point frameStart = WINDOW_STATE::Clock::now();
1144
1145 if( capped && !newInput && !aIgnorePacing && frameStart < ws->nextFrameAt )
1146 {
1147 deferFrameUntil( ws->nextFrameAt );
1148 continue;
1149 }
1150
1151 if( capped )
1152 {
1153 ws->nextFrameAt = RhiNextFrameAt( frameStart, static_cast<double>( ws->refreshHz ),
1154 m_device->LastDisplayRefresh( ws->swapchain ) );
1155 }
1156
1157 if( frameMailbox )
1158 {
1159 // Waiting for the previous present first lets this frame carry the newest input. A frame that already
1160 // has new input goes out with one more present in flight, which the swapchain can still hold.
1161 if( ws->swapchain )
1162 {
1163 const int allowed = RHI_PRESENTS_IN_FLIGHT + ( newInput ? 1 : 0 );
1164
1165 m_device->WaitForPresentSlot( ws->swapchain, std::chrono::milliseconds( 50 ), allowed );
1166 }
1167
1168 if( std::unique_ptr<FRAME_PACKET> packet = frameMailbox->Load() )
1169 {
1170 // A canvas queues a packet's uploads before submitting it, but may have submitted it while this
1171 // thread drew other windows, after the loop's drain. Its buffers must exist before it is drawn.
1173
1174 ws->drawDesc = std::move( packet->drawDesc );
1175 ws->staticDrawDesc = std::move( packet->staticDrawDesc );
1176 ws->dynamicDrawDesc = std::move( packet->dynamicDrawDesc );
1177 ws->staticChanged = packet->staticChanged;
1178 ws->dynamicChanged = packet->dynamicChanged;
1179 ws->staticEpoch = packet->staticEpoch;
1180 ws->dynamicEpoch = packet->dynamicEpoch;
1181 ws->drawnInputSerial = packet->inputSerial;
1182
1183 // Supersampling resizes the layer targets but not the swapchain. The fit keeps a bad
1184 // factor from requesting a huge allocation.
1185 uint32_t factor = fitSupersampleFactor( ws->drawDesc.supersampleFactor, *ws,
1186 m_device->MaxTextureSize() );
1187
1188 if( factor != ws->supersampleFactor )
1189 {
1190 ws->supersampleFactor = factor;
1191 DestroyWindowFBOs( *ws );
1192 CreateWindowFBOs( *ws );
1193
1194 // The new static target is empty
1195 ws->needsStaticInvalidate = true;
1196 }
1197
1198 // A producer that fills only drawDesc needs its frame state copied to the split descs. One
1199 // that numbers its content sets each target's camera itself, since the two can scale depth differently.
1200 const bool ownCameras = packet->staticEpoch != 0;
1201
1202 auto propagateFrameState = [&]( FRAME_DRAW_DESC& aDst )
1203 {
1204 if( !ownCameras )
1205 {
1206 aDst.frameUBO = ws->drawDesc.frameUBO;
1207 aDst.gridUBO = ws->drawDesc.gridUBO;
1208 aDst.drawGrid = aDst.drawGrid || ws->drawDesc.drawGrid;
1209 }
1210
1211 aDst.cursorUBO = ws->drawDesc.cursorUBO;
1212 aDst.drawCursor = aDst.drawCursor || ws->drawDesc.drawCursor;
1213 aDst.linePipelineKind = ws->drawDesc.linePipelineKind;
1214
1215 for( int c = 0; c < 4; ++c )
1216 aDst.backgroundColor[c] = ws->drawDesc.backgroundColor[c];
1217 };
1218
1219 propagateFrameState( ws->staticDrawDesc );
1220 propagateFrameState( ws->dynamicDrawDesc );
1221 }
1222 }
1223
1224 WINDOW_STATE::Clock::time_point now = WINDOW_STATE::Clock::now();
1225 uint64_t pendingGenerations = ws->requestedGeneration >= ws->lastRenderedGeneration
1226 ? ws->requestedGeneration - ws->lastRenderedGeneration
1227 : 0;
1228
1229 if( pendingGenerations >= 60
1230 && WindowStateAgeMs( ws->lastRenderCompleteTime, now ) >= 1000
1231 && ( ws->lastLagLogTime.time_since_epoch().count() == 0
1232 || WindowStateAgeMs( ws->lastLagLogTime, now ) >= 1000 ) )
1233 {
1234 wxLogTrace( traceRhi, wxS( "RenderThread: window backlog windowId=%u pending=%llu "
1235 "reqGen=%llu rendGen=%llu reqAge=%lldms "
1236 "renderAge=%lldms phase=%d" ),
1237 ws->windowId,
1238 static_cast<unsigned long long>( pendingGenerations ),
1239 static_cast<unsigned long long>( ws->requestedGeneration ),
1240 static_cast<unsigned long long>( ws->lastRenderedGeneration ),
1241 WindowStateAgeMs( ws->lastFrameRequestTime, now ),
1242 WindowStateAgeMs( ws->lastRenderCompleteTime, now ),
1243 m_loopPhase.load( std::memory_order_relaxed ) );
1244 ws->lastLagLogTime = now;
1245 }
1246
1247 const uint64_t attemptedGeneration = ws->requestedGeneration;
1248 const uint64_t presentFailuresBefore = ws->presentFailureCount;
1249 const bool presented = RenderWindow( *ws );
1250
1251 if( presented )
1252 {
1253 ws->consecutivePresentFailures = 0;
1254 ws->nextPresentRetryAt = {};
1255 }
1256 else if( ws->frameResultPending && ws->swapchain
1257 && ws->presentFailureCount > presentFailuresBefore )
1258 {
1259 ws->consecutivePresentFailures = std::min( ws->consecutivePresentFailures + 1, 5u );
1260 auto delay = std::chrono::milliseconds( 50u << ( ws->consecutivePresentFailures - 1 ) );
1261 ws->nextPresentRetryAt = WINDOW_STATE::Clock::now() + delay;
1262 deferFrameUntil( ws->nextPresentRetryAt );
1263 ws->active = true;
1264 }
1265
1266 // A skipped or failed present must not acknowledge the request
1267 if( presented && ws->frameResultPending )
1268 {
1269 ws->lastRenderedGeneration = attemptedGeneration;
1270 ws->frameResultPending = false;
1271 }
1272
1273 ws->active = !presented && ws->nextPresentRetryAt.time_since_epoch().count() != 0;
1274 ws->framesProcessed++;
1275 }
1276}
1277
1278
1280{
1281 if( !m_device || IsDeviceLost() )
1282 return false;
1283
1284 aWS.lastRenderStartTime = WINDOW_STATE::Clock::now();
1285 const auto renderStart = aWS.lastRenderStartTime;
1286
1287 // Read only by wxLogTrace, which release builds compile out
1288 [[maybe_unused]] double geometryMs = 0.0;
1289 [[maybe_unused]] double overlayMs = 0.0;
1290 [[maybe_unused]] double compositeMs = 0.0;
1291 [[maybe_unused]] double presentMs = 0.0;
1292 [[maybe_unused]] const double queueWaitMs = WindowStateDurationMs( aWS.lastFrameRequestTime, renderStart );
1293
1294 if( aWS.backingWidth == 0 || aWS.backingHeight == 0 )
1295 {
1296 wxLogTrace( traceRhi, wxS( "RenderThread: RenderWindow windowId=%u SKIP "
1297 "(backing %ux%u)" ),
1298 aWS.windowId, aWS.backingWidth, aWS.backingHeight );
1299 return false;
1300 }
1301
1302 if( !aWS.swapchain )
1303 EnsureSwapchain( aWS, "render" );
1304
1305 if( !m_pipelines || !m_pipelines->IsInitialized() )
1306 return false;
1307
1309
1311 {
1312 m_loopPhase.store( 4, std::memory_order_relaxed );
1313 PROF_TIMER geometryTimer;
1314
1315 // The FBOs behind both targets were recreated or lost
1316 if( aWS.needsStaticInvalidate )
1317 {
1318 aWS.renderedStatic.valid = false;
1319 aWS.renderedDynamic.valid = false;
1320 aWS.renderedOverlay.valid = false;
1321 }
1322
1323 auto holds = []( const WINDOW_STATE::RENDERED_CONTENT& aRendered, uint64_t aEpoch,
1324 const FRAME_DRAW_DESC& aDesc )
1325 {
1326 return aRendered.valid && aRendered.epoch == aEpoch && aRendered.drawGrid == aDesc.drawGrid
1327 && aRendered.frameUBO == aDesc.frameUBO && ( !aDesc.drawGrid || aRendered.gridUBO == aDesc.gridUBO );
1328 };
1329
1330 auto record = []( WINDOW_STATE::RENDERED_CONTENT& aRendered, uint64_t aEpoch,
1331 const FRAME_DRAW_DESC& aDesc )
1332 {
1333 aRendered.valid = true;
1334 aRendered.epoch = aEpoch;
1335 aRendered.drawGrid = aDesc.drawGrid;
1336 aRendered.frameUBO = aDesc.frameUBO;
1337 aRendered.gridUBO = aDesc.gridUBO;
1338 };
1339
1340 // A producer that numbers its content is judged on what the FBO holds, since the packet
1341 // that carried a change flag may have been displaced before it was read
1342 bool renderStatic = aWS.staticFBO
1343 && ( aWS.staticEpoch != 0
1344 ? !holds( aWS.renderedStatic, aWS.staticEpoch, aWS.staticDrawDesc )
1346 && ( aWS.staticChanged || aWS.needsStaticInvalidate ) ) );
1347
1348 // The target holds the current view exactly
1349 if( !renderStatic )
1350 aWS.staticShift = {};
1351
1352 // A static target with a margin shows a pan within it shifted instead of drawing again
1353 uint32_t staticDrawMargin = 0;
1354
1355 if( renderStatic && aWS.staticEpoch != 0 && aWS.staticMargin > 0 )
1356 {
1357 const MARGIN_PLAN plan =
1359
1360 if( plan.reuse )
1361 {
1362 aWS.staticShift = plan.shift;
1363 renderStatic = false;
1364 }
1365
1366 staticDrawMargin = plan.drawMargin;
1367 }
1368
1369 bool renderDynamic = aWS.dynamicFBO
1370 && ( aWS.dynamicEpoch != 0
1371 ? !holds( aWS.renderedDynamic, aWS.dynamicEpoch, aWS.dynamicDrawDesc )
1373
1374 // Cleared only after the decision above so an invalidation queued by HandleSwapchainResize is not lost
1375 aWS.needsStaticInvalidate = false;
1376
1377 if( renderStatic )
1378 {
1379 const bool viewOnly = aWS.staticEpoch != 0 && aWS.staticMargin > 0 && staticDrawMargin == 0;
1380
1381 if( viewOnly && !aWS.staticViewFBO )
1382 aWS.staticViewFBO = m_device->CreateFramebuffer( layerFramebufferDesc( aWS ) );
1383
1384 aWS.staticInMargin = !( viewOnly && aWS.staticViewFBO );
1385
1387 staticDrawMargin );
1388 record( aWS.renderedStatic, aWS.staticEpoch, aWS.staticDrawDesc );
1389 aWS.renderedStatic.margin = staticDrawMargin;
1390 aWS.staticShift = {};
1391 aWS.staticPasses++;
1392 }
1393
1394 if( renderDynamic )
1395 {
1396 const FRAME_DRAW_DESC& dynamic = aWS.dynamicDrawDesc;
1397
1398 // A pan with nothing selected usually leaves the dynamic target empty. Skipping it saves
1399 // a full-size depth copy, a pass and a layer of the composite on every frame.
1401 dynamic.drawGrid || ( dynamic.layerRanges.empty() && dynamic.HasGeometry() )
1402 || std::any_of( dynamic.layerRanges.begin(), dynamic.layerRanges.end(),
1403 []( const LAYER_RANGE& aRange )
1404 {
1405 return aRange.target != TARGET_OVERLAY && aRange.HasGeometry();
1406 } );
1407
1408 if( aWS.dynamicGeometryDrawn )
1409 RenderGeometryInto( aWS, dynamic, aWS.dynamicFBO, false, GEOMETRY_RANGES::NOT_OVERLAY );
1410
1411 // An overlay that cannot reach the view or its margin, as origin markers zoomed away from,
1412 // takes no pass and no composite
1413 const RHI_CULL_VIEW reach = RhiCullView(
1414 aWS.staticMargin > 0 ? RhiWidenFrame( dynamic.frameUBO, aWS.staticMargin ) : dynamic.frameUBO );
1415
1416 const bool hasOverlay = std::any_of( dynamic.layerRanges.begin(), dynamic.layerRanges.end(),
1417 [&]( const LAYER_RANGE& aRange )
1418 {
1419 return aRange.target == TARGET_OVERLAY && aRange.HasGeometry()
1420 && RhiRangeReaches( aRange, reach );
1421 } );
1422
1423 if( !hasOverlay )
1424 aWS.renderedOverlay.valid = false;
1425
1426 if( hasOverlay && !aWS.overlayGeometryFBO )
1427 aWS.overlayGeometryFBO = m_device->CreateFramebuffer( layerFramebufferDesc( aWS ) );
1428
1429 aWS.overlayGeometryDrawn = hasOverlay && aWS.overlayGeometryFBO;
1430
1431 // The overlay keeps a margin too, so a pan with everything selected shows it shifted
1432 // instead of drawing the whole selection again
1433 MARGIN_PLAN overlayPlan;
1434
1435 if( aWS.overlayGeometryDrawn && aWS.dynamicEpoch != 0 && aWS.staticMargin > 0 )
1436 {
1437 overlayPlan = planMarginTarget( aWS.renderedOverlay, aWS.dynamicEpoch, aWS.dynamicDrawDesc,
1438 aWS.staticMargin, false );
1439 }
1440
1441 if( overlayPlan.drawMargin > 0 && !aWS.overlayMarginFBO )
1442 aWS.overlayMarginFBO = m_device->CreateFramebuffer( marginFramebufferDesc( aWS ) );
1443
1444 if( aWS.overlayGeometryDrawn && overlayPlan.reuse )
1445 {
1446 aWS.overlayShift = overlayPlan.shift;
1447 }
1448 else if( aWS.overlayGeometryDrawn )
1449 {
1450 aWS.overlayInMargin = overlayPlan.drawMargin > 0 && aWS.overlayMarginFBO;
1451
1454 GEOMETRY_RANGES::OVERLAY, overlayPlan.drawMargin );
1455 record( aWS.renderedOverlay, aWS.dynamicEpoch, aWS.dynamicDrawDesc );
1456 aWS.renderedOverlay.margin = aWS.overlayInMargin ? overlayPlan.drawMargin : 0;
1457 aWS.overlayShift = {};
1458 }
1459
1460 record( aWS.renderedDynamic, aWS.dynamicEpoch, aWS.dynamicDrawDesc );
1461 aWS.dynamicPasses++;
1462 }
1463
1464 // A packet that fills only drawDesc is drawn into staticFBO. The change flags can be set with
1465 // empty split descs, so this gates on real geometry.
1466 const bool splitDescsEmpty = !aWS.staticDrawDesc.HasGeometry()
1467 && !aWS.dynamicDrawDesc.HasGeometry();
1468
1469 if( splitDescsEmpty && aWS.staticFBO
1470 && !aWS.drawDesc.reuseStaticFBO
1471 && aWS.drawDesc.HasGeometry() )
1472 {
1473 RenderGeometry( aWS );
1474 }
1475
1476 auto hasDifference = []( const FRAME_DRAW_DESC& aDesc )
1477 {
1478 return std::any_of( aDesc.layerRanges.begin(), aDesc.layerRanges.end(),
1479 []( const LAYER_RANGE& aRange )
1480 {
1481 return aRange.difference;
1482 } );
1483 };
1484
1485 // Diff layers come with gerbview's XOR mode, and the copies made for them go when it ends
1486 if( ( aWS.diffBelowFBO || aWS.staticDiffBelowFBO ) && !hasDifference( aWS.staticDrawDesc )
1487 && !hasDifference( aWS.dynamicDrawDesc ) )
1488 {
1489 destroyDiffCopies( *m_device, aWS );
1490 }
1491
1492 geometryMs = geometryTimer.msecs();
1493 }
1494
1495 // The cursor is drawn last in the composite, not into a target of its own and composited
1496 PROF_TIMER overlayTimer;
1497 const bool cursor = prepareCursor( aWS );
1498 overlayMs = overlayTimer.msecs();
1499
1500 RHI_FramebufferHandle compositorOutput;
1501
1502 if( m_compositor && m_compositor->IsInitialized()
1503 && aWS.accumulationFBO )
1504 {
1505 std::vector<COMPOSITOR_LAYER> layers;
1506
1507 if( aWS.staticFBO && aWS.staticInMargin )
1508 layers.push_back( marginLayer( aWS, aWS.staticFBO, aWS.staticShift ) );
1509 else if( aWS.staticFBO )
1510 layers.push_back( { aWS.ShownStaticFBO(), 1.0f } );
1511
1512 if( aWS.dynamicFBO && aWS.dynamicGeometryDrawn )
1513 layers.push_back( { aWS.dynamicFBO, 1.0f } );
1514
1515 if( aWS.overlayGeometryDrawn && aWS.overlayInMargin )
1516 layers.push_back( marginLayer( aWS, aWS.overlayMarginFBO, aWS.overlayShift ) );
1517 else if( aWS.overlayGeometryDrawn )
1518 layers.push_back( { aWS.overlayGeometryFBO, 1.0f } );
1519
1520 m_loopPhase.store( 5, std::memory_order_relaxed );
1521 PROF_TIMER compositeTimer;
1522 compositorOutput = m_compositor->Composite(
1523 aWS.accumulationFBO, aWS.RenderWidth(), aWS.RenderHeight(), layers, aWS.drawDesc.backgroundColor,
1524 [&]()
1525 {
1526 if( cursor )
1527 drawCursor( aWS );
1528 } );
1529 compositeMs = compositeTimer.msecs();
1530 }
1531
1532 // A failed composite, as during the first resize from 1x1, presents the static target so the
1533 // first frame is not blank
1534 RHI_FramebufferHandle presentFBO = compositorOutput ? compositorOutput : aWS.ShownStaticFBO();
1535
1536 // The static target holds a margin around the view, which the fallback leaves out
1537 RHI_SCISSOR_RECT viewOfStatic;
1538 const RHI_SCISSOR_RECT* presentRect = nullptr;
1539
1540 if( !compositorOutput && aWS.staticMargin > 0 && aWS.staticInMargin )
1541 {
1542 viewOfStatic.x = aWS.ShownStaticX();
1543 viewOfStatic.y = aWS.ShownStaticY();
1544 viewOfStatic.width = aWS.RenderWidth();
1545 viewOfStatic.height = aWS.RenderHeight();
1546 presentRect = &viewOfStatic;
1547 }
1548
1549 bool presented = false;
1550
1551 if( aWS.swapchain && presentFBO )
1552 {
1553 m_loopPhase.store( 6, std::memory_order_relaxed );
1554 aWS.lastPresentAttemptTime = WINDOW_STATE::Clock::now();
1555 PROF_TIMER presentTimer;
1556 RHI_FRAME_OP_RESULT presentResult = m_device->Present( aWS.swapchain, presentFBO, presentRect );
1557 presentMs = presentTimer.msecs();
1558
1559 // KICAD_RHI_LOSE_DEVICE=<n> treats the process's n-th present as a lost device to exercise recovery
1560 static const int loseAt = []()
1561 {
1562 const char* value = std::getenv( "KICAD_RHI_LOSE_DEVICE" );
1563 return value ? std::atoi( value ) : 0;
1564 }();
1565
1566 static std::atomic<int> presents{ 0 };
1567
1568 if( presentResult == RHI_FRAME_OP_RESULT::OK && loseAt > 0 && ++presents == loseAt )
1569 presentResult = RHI_FRAME_OP_RESULT::DEVICE_LOST;
1570
1571 if( presentResult == RHI_FRAME_OP_RESULT::DEVICE_LOST )
1572 {
1573 wxLogTrace( traceRhi, wxS( "RenderThread: Device lost during present windowId=%u" ), aWS.windowId );
1574 OnDeviceLost();
1575 return false;
1576 }
1577 else if( presentResult == RHI_FRAME_OP_RESULT::OUT_OF_DATE
1578 || presentResult == RHI_FRAME_OP_RESULT::SURFACE_LOST )
1579 {
1580 aWS.lastPresentFailureTime = WINDOW_STATE::Clock::now();
1581 aWS.presentFailureCount++;
1583 }
1584 else if( presentResult != RHI_FRAME_OP_RESULT::OK )
1585 {
1586 aWS.lastPresentFailureTime = WINDOW_STATE::Clock::now();
1587 aWS.presentFailureCount++;
1588 wxLogTrace( traceRhi, wxS( "RenderThread: Present FAILED windowId=%u "
1589 "srcFBO=%s reqGen=%llu presentFail=%llu "
1590 "result=%d reqAge=%lldms renderAge=%lldms" ),
1591 aWS.windowId, compositorOutput ? "valid" : "null",
1592 static_cast<unsigned long long>( aWS.requestedGeneration ),
1593 static_cast<unsigned long long>( aWS.presentFailureCount ),
1594 static_cast<int>( presentResult ),
1598 aWS.lastPresentFailureTime ) );
1599 }
1600 else
1601 {
1602 aWS.lastPresentSuccessTime = WINDOW_STATE::Clock::now();
1603 aWS.presentSuccessCount++;
1605 presented = true;
1606
1607 if( aWS.presentSuccessCount == 1 )
1608 {
1609 std::lock_guard<std::mutex> lock( m_presentedMutex );
1610 m_presentedWindows.insert( aWS.windowId );
1611 }
1612 }
1613 }
1614
1615 aWS.lastRenderCompleteTime = WINDOW_STATE::Clock::now();
1616
1617 wxLogTrace( traceRhi, wxS( "Perf: RenderWindow windowId=%u gen=%llu total=%.3f "
1618 "queue=%.3f geometry=%.3f overlay=%.3f composite=%.3f "
1619 "present=%.3f reuseStatic=%d tracks=%u arcs=%u "
1620 "pads=%u polys=%u bitmaps=%llu layers=%llu passes=%llu" ),
1621 aWS.windowId,
1622 static_cast<unsigned long long>( aWS.requestedGeneration ),
1624 queueWaitMs, geometryMs, overlayMs, compositeMs, presentMs,
1625 aWS.drawDesc.reuseStaticFBO ? 1 : 0,
1630 static_cast<unsigned long long>( aWS.drawDesc.bitmapDraws.size() ),
1631 static_cast<unsigned long long>( aWS.drawDesc.layerRanges.size() ),
1632 static_cast<unsigned long long>( aWS.geometryRenderPasses ) );
1633
1634 return presented;
1635}
1636
1637
1639{
1640 // For a packet whose geometry is only in drawDesc
1641 aWS.staticInMargin = true;
1642 RenderGeometryInto( aWS, aWS.drawDesc, aWS.staticFBO, true );
1643}
1644
1645
1647 bool aLogSummary, GEOMETRY_RANGES aRanges, uint32_t aStaticDrawMargin )
1648{
1649 if( !aTargetFBO )
1650 return;
1651
1652 const bool overlayPass = aRanges == GEOMETRY_RANGES::OVERLAY;
1653
1654 auto drawsRange = [&]( const LAYER_RANGE& aRange )
1655 {
1656 return aRanges == GEOMETRY_RANGES::ALL || overlayPass == ( aRange.target == TARGET_OVERLAY );
1657 };
1658
1659 // Set while a range whose elements are left premultiplied is drawn
1660 bool premultipliedRange = false;
1661
1662 // Overlay content is blended the way the OpenGL canvas blends its overlay
1663 auto bindPipeline = [&]( RHI_PipelineHandle aPipeline )
1664 {
1665 if( overlayPass )
1666 {
1667 RHI_PipelineHandle premultiplied = m_pipelines->GetPremultipliedVariant( aPipeline );
1668
1669 aPipeline = premultiplied != aPipeline ? premultiplied : m_pipelines->GetOverlayVariant( aPipeline );
1670 }
1671 else if( premultipliedRange )
1672 aPipeline = m_pipelines->GetPremultipliedVariant( aPipeline );
1673
1674 m_device->BindPipeline( aPipeline );
1675
1676 if( m_pipelines->UsesGlyphAtlas( aPipeline ) )
1677 m_device->BindGroup( 1, glyphBindGroup() );
1678 };
1679
1680 if( aLogSummary && aWS.framesProcessed < 5 )
1681 {
1682 wxLogTrace( traceRhi, wxS( "RenderThread: RenderGeometry windowId=%u "
1683 "tracks=%u/%u arcs=%u/%u pads=%u/%u polys=%u bitmaps=%zu "
1684 "grid=%d cursor=%d layers=%zu" ),
1685 aWS.windowId,
1690 dd.bitmapDraws.size(),
1691 dd.drawGrid ? 1 : 0,
1692 dd.drawCursor ? 1 : 0,
1693 dd.layerRanges.size() );
1694 }
1695
1696 // The static and overlay targets are larger than the view by a margin, see WINDOW_STATE::staticMargin.
1697 // Without the margin drawn, the view goes in its middle.
1698 const bool staticTarget = ( aTargetFBO == aWS.staticFBO || aTargetFBO == aWS.overlayMarginFBO )
1699 && aWS.staticMargin > 0;
1700 const bool drawsMargin = staticTarget && aStaticDrawMargin > 0;
1701 const uint32_t targetWidth = staticTarget ? aWS.StaticWidth() : aWS.RenderWidth();
1702 const uint32_t targetHeight = staticTarget ? aWS.StaticHeight() : aWS.RenderHeight();
1703 const uint32_t inset = staticTarget && !drawsMargin ? aWS.staticMargin * aWS.supersampleFactor : 0;
1704
1705 RHI_VIEWPORT viewport;
1706 viewport.x = static_cast<float>( inset );
1707 viewport.y = static_cast<float>( inset );
1708 viewport.width = static_cast<float>( drawsMargin ? targetWidth : aWS.RenderWidth() );
1709 viewport.height = static_cast<float>( drawsMargin ? targetHeight : aWS.RenderHeight() );
1710
1711 RHI_SCISSOR_RECT scissor;
1712 scissor.x = static_cast<int32_t>( inset );
1713 scissor.y = static_cast<int32_t>( inset );
1714 scissor.width = static_cast<uint32_t>( viewport.width );
1715 scissor.height = static_cast<uint32_t>( viewport.height );
1716
1717 const FRAME_UBO_DATA frameData =
1718 RhiSupersampleFrame( drawsMargin ? RhiWidenFrame( dd.frameUBO, aStaticDrawMargin ) : dd.frameUBO,
1719 aWS.supersampleFactor );
1720
1721 const RHI_CULL_VIEW cullView = RhiCullView( frameData );
1722 const GRID_UBO_DATA gridData =
1723 RhiSupersampleGrid( drawsMargin ? RhiWidenGrid( dd.gridUBO, aStaticDrawMargin ) : dd.gridUBO,
1724 aWS.supersampleFactor );
1725
1726 // A temp target is composited texel for texel, so the static target needs one of its own size.
1727 // Made before any pass begins, as creating a target can submit work of its own.
1728 if( staticTarget && !aWS.staticTempFBO
1729 && std::any_of( dd.layerRanges.begin(), dd.layerRanges.end(),
1730 []( const LAYER_RANGE& aRange )
1731 {
1732 return aRange.HasGeometry() && aRange.NeedsTempFBO();
1733 } ) )
1734 {
1736 tempDesc.width = targetWidth;
1737 tempDesc.height = targetHeight;
1738 aWS.staticTempFBO = m_device->CreateFramebuffer( tempDesc );
1739 }
1740
1741 const RHI_FramebufferHandle tempFBO = staticTarget ? aWS.staticTempFBO : aWS.tempFBO;
1742
1743 // A diff layer is differenced against a copy of what is under it, which needs a target of the
1744 // same size as well
1745 RHI_FramebufferHandle& belowFBO = staticTarget ? aWS.staticDiffBelowFBO : aWS.diffBelowFBO;
1746
1747 if( !belowFBO
1748 && std::any_of( dd.layerRanges.begin(), dd.layerRanges.end(),
1749 [&]( const LAYER_RANGE& aRange )
1750 {
1751 return aRange.difference && aRange.HasGeometry() && drawsRange( aRange );
1752 } ) )
1753 {
1754 RHI_FRAMEBUFFER_DESC belowDesc = layerFramebufferDesc( aWS );
1755 belowDesc.width = targetWidth;
1756 belowDesc.height = targetHeight;
1757 belowFBO = m_device->CreateFramebuffer( belowDesc );
1758 }
1759
1760 const bool drawsStatic = aTargetFBO == aWS.staticFBO || aTargetFBO == aWS.staticViewFBO;
1761
1762 WINDOW_STATE::PASS_UNIFORMS& uniforms = overlayPass ? aWS.Uniforms().overlayUniforms
1763 : aTargetFBO == aWS.dynamicFBO ? aWS.Uniforms().dynamicUniforms
1764 : aWS.Uniforms().staticUniforms;
1765
1766 const RHI_BindGroupHandle frameBG = writeUniform( uniforms.frameUBO, uniforms.frameBG,
1767 m_pipelines->GetFrameUBOLayout(), &frameData,
1768 sizeof( FRAME_UBO_DATA ) )
1769 ? uniforms.frameBG
1771
1772 // The font atlas is uploaded before a pass, as a copy cannot be recorded inside one
1774
1775 // Written before the first pass begins. An update between passes would have to wait for
1776 // the ones already submitted.
1777 if( dd.drawGrid )
1778 {
1779 writeUniform( uniforms.gridUBO, uniforms.gridBG, m_pipelines->GetFullscreenUBOLayout(), &gridData,
1780 sizeof( GRID_UBO_DATA ) );
1781 }
1782
1783 // Geometry shaders read their layer's dim from push constants, which hold nothing until set
1784 auto bindGeometry = [&]( RHI_PipelineHandle aPipeline, const RHI_LAYER_DIM& aDim )
1785 {
1786 bindPipeline( aPipeline );
1787 m_device->BindGroup( 0, frameBG );
1788 m_device->SetPushConstants( &aDim, sizeof( aDim ) );
1789 };
1790
1791 auto drawInstanced = [&]( RHI_PipelineHandle aPipeline, RHI_BufferHandle aInstanceBuffer, uint32_t aInstanceCount,
1792 uint32_t aFirstInstance = 0, const RHI_LAYER_DIM& aDim = {} )
1793 {
1794 if( !aPipeline || !aInstanceBuffer || aInstanceCount == 0 || !frameBG )
1795 return;
1796
1797 bindGeometry( aPipeline, aDim );
1798 m_device->BindVertexBuffer( 1, aInstanceBuffer );
1799
1801 {
1802 m_device->BindVertexBuffer( 0, m_quadCornerBuffer );
1803 m_device->BindIndexBuffer( m_quadIndexBuffer );
1804 m_device->DrawIndexedInstanced( 6, aInstanceCount, 0, 0, aFirstInstance );
1805 }
1806 else
1807 {
1808 m_device->BindVertexBuffer( 0, m_unitQuadBuffer );
1809 m_device->DrawInstanced( 6, aInstanceCount, 0, 0, aFirstInstance );
1810 }
1811 };
1812
1813 auto drawBitmap = [&]( const BITMAP_DRAW_DESC& aBitmap )
1814 {
1815 if( !frameBG || !m_pipelines->GetBitmapPipeline()
1816 || !aBitmap.vertexBuffer || !aBitmap.texture )
1817 {
1818 return;
1819 }
1820
1821 // The overlay pass needs the variant that also blends alpha
1822 bindPipeline( m_pipelines->GetBitmapPipeline() );
1823 m_device->BindGroup( 0, frameBG );
1824
1825 RHI_BIND_GROUP_DESC texBGDesc;
1826 texBGDesc.layout = m_pipelines->GetTextureSamplerLayout();
1827 texBGDesc.entries.push_back( { 0, {}, aBitmap.texture, 0, 0 } );
1828 RHI_BindGroupHandle texBG = m_device->CreateBindGroup( texBGDesc );
1829
1830 if( !texBG )
1831 return;
1832
1833 m_device->BindGroup( 1, texBG );
1834 m_device->BindVertexBuffer( 0, aBitmap.vertexBuffer );
1835 m_device->Draw( aBitmap.vertexCount );
1836 m_device->DestroyBindGroup( texBG );
1837 };
1838
1839 // Schematic wires are drawn as tracks by shaders of their own, where they have them
1840 const bool wires = dd.linePipelineKind == RHI_LINE_PIPELINE_KIND::SCH_WIRES;
1841
1842 auto shapePipeline = [&]( RHI_SHAPE aShape, LAYER_DRAW_MODE aMode )
1843 {
1844 if( wires && aShape == RHI_SHAPE_TRACK && aMode != LAYER_DRAW_MODE::MARK )
1845 {
1846 return aMode == LAYER_DRAW_MODE::ONCE ? m_pipelines->GetWireOncePipeline()
1847 : m_pipelines->GetWirePipeline();
1848 }
1849
1850 return m_pipelines->GetShapePipeline( aShape, aMode );
1851 };
1852
1853 auto drawGrid = [&]()
1854 {
1855 if( !dd.drawGrid || !m_pipelines->GetGridPipeline() || !uniforms.gridBG || !m_unitQuadBuffer )
1856 return;
1857
1858 m_device->BindPipeline( m_pipelines->GetGridPipeline() );
1859 m_device->BindGroup( 0, uniforms.gridBG );
1860 m_device->BindVertexBuffer( 0, m_unitQuadBuffer );
1861 m_device->Draw( 6 );
1862 };
1863
1864 auto drawLayerGeometry = [&]( const LAYER_RANGE& aRange )
1865 {
1866 premultipliedRange = aRange.overlayAlpha;
1867
1868 auto pick = []( RHI_BufferHandle aOwn, RHI_BufferHandle aShared )
1869 {
1870 return aOwn ? aOwn : aShared;
1871 };
1872
1873 // A layer that keeps relative depth is drawn into its own slice of the depth range.
1874 // With knockouts marked in depth the slice is halved, marks in front.
1875 const float mid = 0.5f * ( aRange.depthNear + aRange.depthFar );
1876
1877 auto setSlice = [&]( bool aNearHalf, bool aOrdered )
1878 {
1879 RHI_VIEWPORT slice = viewport;
1880 slice.minDepth = ( aRange.depthKnockout && !aNearHalf ) ? mid : aRange.depthNear;
1881 slice.maxDepth = ( aRange.depthKnockout && aNearHalf ) ? mid : aRange.depthFar;
1882
1883 // Runs are ordered by when they are drawn. Flattened to one depth, none of them can
1884 // lose the depth test to a rounding difference between element types.
1885 if( aOrdered )
1886 slice.minDepth = slice.maxDepth;
1887
1888 m_device->SetViewport( slice );
1889 };
1890
1891 for( const LAYER_DRAW_STEP& step : RhiPlanLayerDraws( aRange, dd.polarityRuns ) )
1892 {
1893 if( step.count == 0 )
1894 continue;
1895
1896 // Polygons have no polarity flag, so their negative runs share one buffer with the rest
1897 const bool polygon = step.shape == RHI_SHAPE_POLYGON;
1898 const bool negatives = step.negatives && !polygon;
1899
1900 const RHI_PipelineHandle pipeline = shapePipeline( step.shape, step.mode );
1901 const RHI_BufferHandle own = negatives ? aRange.negBuffer[step.shape] : aRange.buffer[step.shape];
1902 const RHI_BufferHandle buffer = pick( own, dd.buffer[step.shape] );
1903
1904 const std::vector<RHI_RUN_BOUNDS>* runs =
1905 aRange.cullRuns ? &aRange.cullRuns->Of( step.shape, negatives ) : nullptr;
1906
1907 if( polygon )
1908 {
1909 if( !frameBG || !pipeline || !buffer )
1910 continue;
1911
1912 bindGeometry( pipeline, aRange.dim );
1913 m_device->BindVertexBuffer( 0, buffer );
1914 }
1915
1916 setSlice( step.nearHalf, step.ordered );
1917
1918 RhiForVisibleStretches( runs, RhiCullRunLength( step.shape ), step.start, step.count, cullView,
1919 [&]( uint32_t aFirst, uint32_t aCount )
1920 {
1921 if( polygon )
1922 m_device->Draw( aCount, aFirst );
1923 else
1924 drawInstanced( pipeline, buffer, aCount, aFirst, aRange.dim );
1925 } );
1926 }
1927
1928 uint32_t bmpEnd = aRange.bitmapStart + aRange.bitmapCount;
1929
1930 if( bmpEnd > dd.bitmapDraws.size() )
1931 bmpEnd = static_cast<uint32_t>( dd.bitmapDraws.size() );
1932
1933 // A range holding only bitmaps has set no depth slice of its own yet
1934 if( aRange.bitmapStart < bmpEnd )
1935 setSlice( false, false );
1936
1937 for( uint32_t bi = aRange.bitmapStart; bi < bmpEnd; ++bi )
1938 drawBitmap( dd.bitmapDraws[bi] );
1939 };
1940
1941 auto beginPass = [&]( RHI_FramebufferHandle aFBO, RHI_RenderPassHandle aRenderPass ) -> bool
1942 {
1943 if( !aFBO || !aRenderPass )
1944 return false;
1945
1947 rpBegin.renderPass = aRenderPass;
1948 rpBegin.framebuffer = aFBO;
1949
1950 if( !m_device->BeginRenderPass( rpBegin ) )
1951 return false;
1952
1954
1955 m_device->SetViewport( viewport );
1956 m_device->SetScissor( scissor );
1957 return true;
1958 };
1959
1960 // Covers the whole target because the temp target matches it texel for texel, whatever part of it
1961 // was drawn. Each texture goes to the binding of its place in the list.
1962 auto drawWhole = [&]( RHI_BindGroupLayoutHandle aLayout, std::initializer_list<RHI_TextureHandle> aTextures,
1963 float aAlpha ) -> bool
1964 {
1965 RHI_BufferHandle quadVB = m_compositor->GetQuadVB();
1966
1967 if( !quadVB )
1968 return false;
1969
1970 RHI_BIND_GROUP_DESC bgDesc;
1971 bgDesc.layout = aLayout;
1972
1973 for( RHI_TextureHandle tex : aTextures )
1974 {
1975 if( !tex )
1976 return false;
1977
1978 bgDesc.entries.push_back( { static_cast<uint32_t>( bgDesc.entries.size() ), {}, tex, 0, 0 } );
1979 }
1980
1981 RHI_BindGroupHandle bg = m_device->CreateBindGroup( bgDesc );
1982
1983 if( !bg )
1984 return false;
1985
1986 RHI_VIEWPORT whole;
1987 whole.width = static_cast<float>( targetWidth );
1988 whole.height = static_cast<float>( targetHeight );
1989
1990 RHI_SCISSOR_RECT wholeScissor;
1991 wholeScissor.width = targetWidth;
1992 wholeScissor.height = targetHeight;
1993
1995 pc.alpha = aAlpha;
1996
1997 m_device->SetViewport( whole );
1998 m_device->SetScissor( wholeScissor );
1999 m_device->BindVertexBuffer( 0, quadVB );
2000 m_device->BindGroup( 0, bg );
2001 m_device->SetPushConstants( &pc, sizeof( pc ) );
2002 m_device->Draw( 6 );
2003 m_device->DestroyBindGroup( bg );
2004 m_device->SetViewport( viewport );
2005 m_device->SetScissor( scissor );
2006 return true;
2007 };
2008
2009 auto compositeTemp = [&]( float aAlpha )
2010 {
2011 if( !m_compositor || !m_compositor->IsInitialized() || !m_pipelines->GetInlineCompositorPipeline() )
2012 return;
2013
2014 bindPipeline( m_pipelines->GetInlineCompositorPipeline() );
2015 drawWhole( m_compositor->GetSingleTexLayout(), { m_device->GetFramebufferColorTexture( tempFBO, 0 ) }, aAlpha );
2016 };
2017
2018 // A pass cannot sample the target it draws into, so a diff layer reads a copy of it
2019 auto copyBelow = [&]() -> bool
2020 {
2021 if( !belowFBO || !m_pipelines->GetDifferencePipeline()
2024 {
2025 return false;
2026 }
2027
2028 // Premultiplied over a target cleared to transparent, at alpha 1, is an exact copy
2029 m_device->BindPipeline( m_pipelines->GetInlineCompositorPipeline() );
2030
2031 const bool copied = drawWhole( m_compositor->GetSingleTexLayout(),
2032 { m_device->GetFramebufferColorTexture( aTargetFBO, 0 ) }, 1.0f );
2033
2034 m_device->EndRenderPass();
2035 return copied;
2036 };
2037
2038 auto compositeDifference = [&]( float aAlpha )
2039 {
2040 m_device->BindPipeline( m_pipelines->GetDifferencePipeline() );
2041 drawWhole( m_pipelines->GetDifferenceLayout(),
2042 { m_device->GetFramebufferColorTexture( tempFBO, 0 ),
2043 m_device->GetFramebufferColorTexture( belowFBO, 0 ) },
2044 aAlpha );
2045 };
2046
2047 auto renderLegacy = [&]()
2048 {
2049 if( !beginPass( aTargetFBO, m_geometryRenderPass ) )
2050 return;
2051
2053 {
2054 const bool negatives = mode == LAYER_DRAW_MODE::NEGATIVE;
2055
2056 for( int shape = 0; shape < RHI_INSTANCED_SHAPES; ++shape )
2057 {
2058 drawInstanced( shapePipeline( RHI_SHAPE( shape ), mode ), dd.buffer[shape],
2059 negatives ? dd.negCount[shape] : dd.count[shape], negatives ? dd.count[shape] : 0 );
2060 }
2061 }
2062
2063 RHI_PipelineHandle zonePipeline = shapePipeline( RHI_SHAPE_POLYGON, LAYER_DRAW_MODE::BLEND );
2064
2065 if( frameBG && zonePipeline && dd.buffer[RHI_SHAPE_POLYGON] && dd.count[RHI_SHAPE_POLYGON] > 0 )
2066 {
2067 bindGeometry( zonePipeline, {} );
2068 m_device->BindVertexBuffer( 0, dd.buffer[RHI_SHAPE_POLYGON] );
2069 m_device->Draw( dd.count[RHI_SHAPE_POLYGON] );
2070 }
2071
2072 for( const auto& bmp : dd.bitmapDraws )
2073 drawBitmap( bmp );
2074
2075 drawGrid();
2076 m_device->EndRenderPass();
2077 };
2078
2079 auto renderLayered = [&]()
2080 {
2081 // The view sits in the static target where its margin and the pan since it was drawn put it
2082 const uint32_t staticX = static_cast<uint32_t>( aWS.ShownStaticX() );
2083 const uint32_t staticY = static_cast<uint32_t>( aWS.ShownStaticY() );
2084
2085 // Content kept apart from the static target is still ordered among its layers, by
2086 // testing against the depth the static target was drawn with
2087 const bool overStatic = dd.testAgainstStatic && !overlayPass && aWS.staticFBO && !drawsStatic
2089 && m_device->CopyFramebufferDepth( aWS.ShownStaticFBO(), aTargetFBO, staticX,
2090 staticY );
2091
2092 auto usesTemp = [&]( const LAYER_RANGE& aRange )
2093 {
2094 return aRange.HasGeometry() && drawsRange( aRange ) && aRange.NeedsTempFBO() && tempFBO
2095 && m_geometryLoadRenderPass && m_pipelines->GetInlineCompositorPipeline() && m_compositor
2096 && m_compositor->IsInitialized();
2097 };
2098
2099 // The static target's depth is copied to the dynamic one, and a pass that a temp layer
2100 // splits is resumed from its depth. Otherwise the depth ends with the pass.
2101 const bool keepDepth = drawsStatic || std::any_of( dd.layerRanges.begin(), dd.layerRanges.end(), usesTemp );
2102
2104
2107
2108 bool inStaticPass = beginPass( aTargetFBO, firstPass );
2109
2110 if( !inStaticPass )
2111 return;
2112
2113 const bool drawsGrid = dd.drawGrid && !overlayPass;
2114
2115 if( drawsGrid && dd.gridFirst )
2116 drawGrid();
2117
2118 for( const LAYER_RANGE& lr : dd.layerRanges )
2119 {
2120 if( !lr.HasGeometry() || !drawsRange( lr ) )
2121 continue;
2122
2123 const bool useTempFBO = usesTemp( lr );
2124
2125 if( !useTempFBO )
2126 {
2127 if( !inStaticPass )
2128 {
2129 inStaticPass = beginPass( aTargetFBO, m_geometryLoadRenderPass );
2130
2131 if( !inStaticPass )
2132 return;
2133 }
2134
2135 drawLayerGeometry( lr );
2136 continue;
2137 }
2138
2139 if( inStaticPass )
2140 {
2141 m_device->EndRenderPass();
2142 inStaticPass = false;
2143 }
2144
2145 RHI_RenderPassHandle tempPass =
2147
2148 // Composited over the whole target, a diff layer would land in front of the static
2149 // content that hides it. Drawn against the target's depth it leaves that uncovered.
2150 if( lr.difference && overStatic && m_geometryDepthLoadDiscardRenderPass
2151 && m_device->CopyFramebufferDepth( aTargetFBO, tempFBO ) )
2152 {
2154 }
2155
2156 if( !beginPass( tempFBO, tempPass ) )
2157 continue;
2158
2159 drawLayerGeometry( lr );
2160 m_device->EndRenderPass();
2161
2162 const bool difference = lr.difference && copyBelow();
2163
2164 if( !beginPass( aTargetFBO, m_geometryLoadRenderPass ) )
2165 return;
2166
2167 if( difference )
2168 compositeDifference( lr.alpha );
2169 else
2170 compositeTemp( lr.alpha );
2171
2172 m_device->EndRenderPass();
2173 }
2174
2175 if( drawsGrid && !dd.gridFirst )
2176 {
2177 if( !inStaticPass )
2178 {
2179 inStaticPass = beginPass( aTargetFBO, m_geometryLoadRenderPass );
2180
2181 if( !inStaticPass )
2182 return;
2183 }
2184
2185 drawGrid();
2186 }
2187
2188 if( inStaticPass )
2189 m_device->EndRenderPass();
2190 };
2191
2192 if( !dd.layerRanges.empty() )
2193 renderLayered();
2194 else if( !overlayPass )
2195 renderLegacy();
2196}
2197
2198
2200{
2201 const FRAME_DRAW_DESC& dd = aWS.drawDesc;
2202
2203 if( !dd.drawCursor || !m_pipelines || !m_pipelines->GetCursorCompositePipeline() )
2204 return false;
2205
2206 WINDOW_STATE::FRAME_UNIFORMS& uniforms = aWS.Uniforms();
2207
2208 return writeUniform( uniforms.cursorUBO, uniforms.cursorBG, m_pipelines->GetFullscreenUBOLayout(),
2209 &dd.cursorUBO, sizeof( CURSOR_UBO_DATA ) )
2211}
2212
2213
2215 RHI_BindGroupLayoutHandle aLayout, const void* aData, size_t aSize )
2216{
2217 if( !aUBO )
2218 {
2219 // Host visible, so the per-frame update is a copy and not a staged GPU upload
2220 RHI_BUFFER_DESC desc;
2222 desc.sizeBytes = aSize;
2223 desc.mappable = true;
2224 aUBO = m_device->CreateBuffer( desc );
2225
2226 if( !aUBO )
2227 return false;
2228 }
2229
2230 m_device->UpdateBuffer( aUBO, aData, aSize );
2231
2232 if( !aBG )
2233 {
2234 RHI_BIND_GROUP_DESC bgDesc;
2235 bgDesc.layout = aLayout;
2236 bgDesc.entries.push_back( { RHI_UBO_BINDING, aUBO, {}, 0, static_cast<uint32_t>( aSize ) } );
2237 aBG = m_device->CreateBindGroup( bgDesc );
2238 }
2239
2240 return static_cast<bool>( aBG );
2241}
2242
2243
2245{
2246 m_device->BindPipeline( m_pipelines->GetCursorCompositePipeline() );
2247 m_device->BindGroup( 0, aWS.Uniforms().cursorBG );
2248 m_device->BindVertexBuffer( 0, m_unitQuadBuffer );
2249 m_device->Draw( 6 );
2250}
2251
2252
2254{
2255 wxLogTrace( traceRhi, wxS( "RenderThread: SwapchainCreate windowId=%u %ux%u dpi=%.1f "
2256 "native=%p" ),
2257 aCmd.windowId, aCmd.width, aCmd.height, aCmd.dpiScale,
2259
2260 if( m_windows.count( aCmd.windowId ) )
2261 {
2262 wxLogTrace( traceRhi, wxS( "RenderThread: SwapchainCreate SKIPPED (window already "
2263 "exists)" ) );
2264 return;
2265 }
2266
2267 WINDOW_STATE ws;
2268 ws.windowId = aCmd.windowId;
2269 ws.nativeHandle = aCmd.nativeHandle;
2270 ws.width = aCmd.width;
2271 ws.height = aCmd.height;
2272 ws.dpiScale = aCmd.dpiScale;
2273
2275 ws.backingWidth, ws.backingHeight );
2276
2277 wxLogTrace( traceRhi, wxS( "RenderThread: backing resolution %ux%u" ),
2278 ws.backingWidth, ws.backingHeight );
2279
2280 RHI_SURFACE_DESC surfDesc;
2281 surfDesc.window = aCmd.nativeHandle;
2282 ws.surface = m_device->CreateSurface( surfDesc );
2283
2284 wxLogTrace( traceRhi, wxS( "RenderThread: surface=%s" ),
2285 ws.surface ? "OK" : "FAILED" );
2286
2287 if( ws.surface )
2288 EnsureSwapchain( ws, "create" );
2289
2290 ws.mailbox = aCmd.frame2DMailbox;
2291
2292 m_windows.emplace( aCmd.windowId, std::move( ws ) );
2293
2294 auto it = m_windows.find( aCmd.windowId );
2295 CreateWindowFBOs( it->second );
2296
2297 wxLogTrace( traceRhi, wxS( "RenderThread: window %u created (FBOs: static=%s dynamic=%s)" ),
2298 aCmd.windowId,
2299 it->second.staticFBO ? "OK" : "null",
2300 it->second.dynamicFBO ? "OK" : "null" );
2301}
2302
2303
2305{
2306 auto it = m_windows.find( aCmd.windowId );
2307
2308 if( it == m_windows.end() )
2309 {
2310 wxLogTrace( traceRhi, wxS( "RenderThread: SwapchainResize windowId=%u NOT FOUND" ),
2311 aCmd.windowId );
2312 return;
2313 }
2314
2315 WINDOW_STATE& ws = it->second;
2316 ws.nextPresentRetryAt = {};
2317 ws.width = aCmd.width;
2318 ws.height = aCmd.height;
2319 ws.dpiScale = aCmd.dpiScale;
2320
2321 uint32_t newBackingW = 0;
2322 uint32_t newBackingH = 0;
2323 ComputeBackingResolution( aCmd.width, aCmd.height, aCmd.dpiScale, newBackingW, newBackingH );
2324
2325 bool backingChanged = ( newBackingW != ws.backingWidth || newBackingH != ws.backingHeight );
2326
2327 wxLogTrace( traceRhi, wxS( "RenderThread: SwapchainResize windowId=%u %ux%u -> %ux%u "
2328 "(backing %ux%u -> %ux%u, changed=%s)" ),
2329 aCmd.windowId, ws.backingWidth, ws.backingHeight,
2330 aCmd.width, aCmd.height, ws.backingWidth, ws.backingHeight,
2331 newBackingW, newBackingH, backingChanged ? "yes" : "no" );
2332
2333 ws.backingWidth = newBackingW;
2334 ws.backingHeight = newBackingH;
2335
2336 if( ws.swapchain )
2337 {
2338 bool resizeOk = m_device->ResizeSwapchain( ws.swapchain, newBackingW, newBackingH,
2339 aCmd.dpiScale );
2340
2341 wxLogTrace( traceRhi, wxS( "RenderThread: ResizeSwapchain windowId=%u %ux%u -> %s" ),
2342 aCmd.windowId, newBackingW, newBackingH,
2343 resizeOk ? "OK" : "FAILED" );
2344
2345 if( !resizeOk )
2346 {
2347 // A failed resize leaves the swapchain at an indeterminate size. A failed recreation
2348 // leaves it null for the frame loop to retry.
2350 }
2351 }
2352 else if( ws.surface )
2353 {
2354 EnsureSwapchain( ws, "resize" );
2355 }
2356
2357 // Targets that cannot be made stay null, so the window is not drawn at a stale size
2358 if( backingChanged )
2359 {
2360 DestroyWindowFBOs( ws );
2361 CreateWindowFBOs( ws );
2362 }
2363
2364 ws.needsStaticInvalidate = true;
2365}
2366
2367
2369{
2370 wxLogTrace( traceRhi, wxS( "RenderThread: SwapchainDestroy windowId=%u" ), aCmd.windowId );
2371
2372 auto it = m_windows.find( aCmd.windowId );
2373
2374 if( it == m_windows.end() )
2375 {
2376 wxLogTrace( traceRhi, wxS( "RenderThread: SwapchainDestroy NOT FOUND" ) );
2377 return;
2378 }
2379
2380 destroyWindow( it->second );
2381 m_windows.erase( it );
2382}
2383
2384
2386{
2387 // Uploads travel on their own queue, so the ones sent ahead of the fence are applied here.
2388 // Otherwise the fence can fire first and its sender reads buffers that are not yet written.
2390
2391 // Frames requested ahead of the fence are drawn before it fires, so its sender sees their results.
2392 // Refresh pacing would hold them back, so it does not apply here. A plain fence skips this, so closing
2393 // one canvas does not wait on drawing the others that share the thread.
2394 if( aCmd.flushFrames
2395 && std::ranges::any_of( m_windows, []( const auto& aEntry ) { return aEntry.second.active; } ) )
2396 {
2397 ProcessActiveWindows( true );
2398 }
2399
2400 if( aCmd.onComplete )
2401 aCmd.onComplete();
2402}
2403
2404
2406{
2407 // Without a device the resources died with it, so only the bookkeeping is cleared
2408 if( m_device )
2409 {
2410 for( auto& [id, ws] : m_windows )
2411 destroyWindow( ws );
2412 }
2413
2414 m_windows.clear();
2415}
2416
2417
2419{
2420 DestroyWindowFBOs( aWS );
2421 DestroyWindowUniforms( aWS );
2422 m_device->Release( aWS.swapchain );
2423 m_device->Release( aWS.surface );
2424}
2425
2426
2428{
2429 if( aWS.backingWidth == 0 || aWS.backingHeight == 0 )
2430 return;
2431
2432 // A resize can grow the window past what the current factor fits
2433 aWS.supersampleFactor = fitSupersampleFactor( aWS.supersampleFactor, aWS, m_device->MaxTextureSize() );
2434
2435 const RHI_FRAMEBUFFER_DESC fbDesc = layerFramebufferDesc( aWS );
2436
2437 aWS.staticMargin = staticMarginFor( aWS, m_device->MaxTextureSize() );
2438 aWS.staticShift = {};
2439 aWS.overlayShift = {};
2440 aWS.renderedStatic.valid = false;
2441 aWS.renderedOverlay.valid = false;
2442
2443 const RHI_FRAMEBUFFER_DESC staticDesc = marginFramebufferDesc( aWS );
2444
2445 aWS.staticFBO = m_device->CreateFramebuffer( staticDesc );
2446 aWS.dynamicFBO = m_device->CreateFramebuffer( fbDesc );
2447 aWS.tempFBO = m_device->CreateFramebuffer( fbDesc );
2448
2449 RHI_FRAMEBUFFER_DESC compFBDesc;
2450 compFBDesc.width = aWS.RenderWidth();
2451 compFBDesc.height = aWS.RenderHeight();
2452 compFBDesc.colorFormats.push_back( RHI_TEXTURE_FORMAT::RGBA8 );
2453 compFBDesc.hasDepthStencil = false;
2454
2455 aWS.accumulationFBO = m_device->CreateFramebuffer( compFBDesc );
2456}
2457
2458
2460{
2461 // The compositor caches bind groups over these targets' textures, so stale ones would pile up
2462 if( m_compositor )
2463 m_compositor->InvalidateCachedBindGroups();
2464
2465 for( RHI_FramebufferHandle* fbo : { &aWS.staticViewFBO, &aWS.staticFBO, &aWS.dynamicFBO, &aWS.tempFBO,
2467 &aWS.accumulationFBO } )
2468 {
2469 m_device->Release( *fbo );
2470 }
2471
2472 destroyDiffCopies( *m_device, aWS );
2473
2474 aWS.staticInMargin = true;
2475 aWS.overlayGeometryDrawn = false;
2476 aWS.overlayInMargin = false;
2477}
2478
2479
2481{
2483 {
2484 for( WINDOW_STATE::PASS_UNIFORMS* uniforms :
2485 { &set.staticUniforms, &set.dynamicUniforms, &set.overlayUniforms } )
2486 {
2487 m_device->Release( uniforms->frameBG );
2488 m_device->Release( uniforms->gridBG );
2489 m_device->Release( uniforms->frameUBO );
2490 m_device->Release( uniforms->gridUBO );
2491 }
2492
2493 m_device->Release( set.cursorBG );
2494 m_device->Release( set.cursorUBO );
2495 }
2496}
2497
2498
2499void RENDER_THREAD::ComputeBackingResolution( uint32_t aLogicalW, uint32_t aLogicalH,
2500 float aDpiScale,
2501 uint32_t& aBackingW, uint32_t& aBackingH )
2502{
2503 aBackingW = static_cast<uint32_t>( std::floor( static_cast<float>( aLogicalW ) * aDpiScale ) );
2504 aBackingH = static_cast<uint32_t>( std::floor( static_cast<float>( aLogicalH ) * aDpiScale ) );
2505
2506 if( aBackingW == 0 )
2507 aBackingW = 1;
2508
2509 if( aBackingH == 0 )
2510 aBackingH = 1;
2511}
2512
2513
2515{
2516 wxLogTrace( traceRhi, wxS( "RenderThread: device lost, leaving recovery to the canvas" ) );
2517
2518 m_uploadQueue.clear();
2519 m_deviceLost.store( true, std::memory_order_release );
2520
2521 // The owners may otherwise not look until something else asks for a frame
2522 std::lock_guard<std::mutex> lock( m_deviceLostMutex );
2523
2524 for( const auto& [owner, listener] : m_deviceLostListeners )
2525 listener();
2526}
2527
2528
2530{
2531 std::lock_guard<std::mutex> lock( m_presentedMutex );
2532 return m_presentedWindows.contains( aWindowId );
2533}
2534
2535
2536void RENDER_THREAD::AddDeviceLostListener( const void* aOwner, std::function<void()> aListener )
2537{
2538 std::lock_guard<std::mutex> lock( m_deviceLostMutex );
2539 m_deviceLostListeners[aOwner] = std::move( aListener );
2540}
2541
2542
2544{
2545 std::lock_guard<std::mutex> lock( m_deviceLostMutex );
2546 m_deviceLostListeners.erase( aOwner );
2547}
2548
2549
2550} // namespace KIGFX
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
Lock-free single-slot mailbox carrying frame state from the main thread to the render thread.
std::unique_ptr< FRAME_PACKET > Load()
uint64_t PendingInputSerial() const
void CreateWindowFBOs(WINDOW_STATE &aWS)
RHI_BufferHandle m_unitQuadBuffer
RHI_BufferHandle m_quadIndexBuffer
its 2 triangles over those corners
std::mutex m_glyphAtlasMutex
std::unique_ptr< RHI_DEVICE > m_device
SYNC_QUEUE< RHI_UPLOAD_COMMAND > m_uploadQueue
bool prepareCursor(WINDOW_STATE &aWS)
Fill the cursor's uniforms.
std::unordered_set< RHI_WINDOW_ID > m_presentedWindows
bool WaitForFence(std::chrono::milliseconds aTimeout=std::chrono::milliseconds(2000))
Block until every command submitted before this call has been processed.
void ProcessActiveWindows(bool aIgnorePacing=false)
std::unordered_map< RHI_WINDOW_ID, WINDOW_STATE > m_windows
RHI_BufferHandle m_quadCornerBuffer
the unit quad's 4 corners
void ThreadMain(std::promise< bool > aStarted)
RHI_DEVICE_FACTORY m_deviceFactory
void deferFrameUntil(WINDOW_STATE::Clock::time_point aDue)
bool EnsureSwapchain(WINDOW_STATE &aWS, const char *aReason)
RHI_RenderPassHandle m_geometryRenderPass
void HandleSwapchainDestroy(const RHI_CMD_SWAPCHAIN_DESTROY &aCmd)
std::optional< WINDOW_STATE::Clock::time_point > m_deferredFrameDue
Earliest time a window held to its display's refresh rate is due, so the loop wakes for it.
RHI_RenderPassHandle m_geometryDepthLoadRenderPass
clears colour, keeps depth
void AddDeviceLostListener(const void *aOwner, std::function< void()> aListener)
Call aListener on the render thread when the device is lost.
bool runFenced(std::function< void()> aWork, std::chrono::milliseconds aTimeout)
Run work on the render thread after everything queued before it.
std::atomic< bool > m_uploadsPending
Set by producers and cleared by the drain so the loop can sleep without missing uploads.
bool m_acceptingWork
Guarded by m_workMutex.
void HandleSwapchainResize(const RHI_CMD_SWAPCHAIN_RESIZE &aCmd)
std::mutex m_presentedMutex
Window IDs are never reused, so an entry outliving its window is harmless.
std::atomic< bool > m_running
std::mutex m_deviceMutex
Guards the m_device pointer, not the device, against UI thread reads racing its creation or destructi...
void RemoveDeviceLostListener(const void *aOwner)
Once this returns, the owner's listener is not running and will not be called.
RHI_RenderPassHandle m_geometryLoadRenderPass
static void ComputeBackingResolution(uint32_t aLogicalW, uint32_t aLogicalH, float aDpiScale, uint32_t &aBackingW, uint32_t &aBackingH)
Compute the physical pixel backing size from logical size and DPI scale, at least 1x1.
void DestroyWindowUniforms(WINDOW_STATE &aWS)
bool IsDeviceLost() const
void SetGlyphAtlas(std::shared_ptr< const RHI_GLYPH_ATLAS_DATA > aAtlas)
Give the track shaders the font atlas glyph instances draw from.
void SubmitUpload(RHI_UPLOAD_COMMAND aCmd)
Callable from any thread.
RHI_TextureHandle m_glyphTexture
The font atlas the track shaders draw glyphs from, its glyph table and their bind group.
bool HasPresented(RHI_WINDOW_ID aWindowId) const
RHI_BufferHandle m_glyphTable
bool ReadbackWindowPixels(RHI_WINDOW_ID aWindowId, wxImage &aImage)
Read back a window's picture at backing resolution without the overlay and cursor.
std::atomic< uint32_t > m_maxTextureSize
std::atomic< bool > m_shutdownRequested
std::atomic< RHI_BACKEND > m_backendType
void HandleFence(const RHI_CMD_FENCE &aCmd)
void HandleSwapchainCreate(const RHI_CMD_SWAPCHAIN_CREATE &aCmd)
bool writeUniform(RHI_BufferHandle &aUBO, RHI_BindGroupHandle &aBG, RHI_BindGroupLayoutHandle aLayout, const void *aData, size_t aSize)
Write a uniform block, making its buffer and bind group on first use.
std::atomic< bool > m_deviceLost
RHI_BindGroupHandle m_glyphBG
RHI_BindGroupHandle glyphBindGroup()
RHI_BufferHandle ReserveBufferHandle()
Reserve a handle without creating the GPU resource, so RHI_DEVICE_PROXY can hand it out on the UI thr...
RHI_TextureHandle ReserveTextureHandle()
RHI_RenderPassHandle m_geometryDepthDiscardRenderPass
As m_geometryRenderPass and m_geometryDepthLoadRenderPass, without storing depth nothing reads later.
GEOMETRY_RANGES
The ranges of a draw descriptor one geometry pass draws.
RENDER_THREAD(RHI_DEVICE_FACTORY aDeviceFactory)
void OnDeviceLost()
Stop drawing on a lost device.
void HandleFrameRequest(const RHI_CMD_FRAME_REQUEST &aCmd)
std::mutex m_deviceLostMutex
Held while a listener runs, so removing one waits for it to return.
void Shutdown()
Block until the render thread exits.
std::unordered_map< const void *, std::function< void()> > m_deviceLostListeners
std::condition_variable m_workCV
void recreateSwapchainAfterOutOfDate(WINDOW_STATE &aWS, bool aSurfaceLost=false)
Retire and recreate an out of date swapchain, and its surface too when aSurfaceLost.
std::unique_ptr< RHI_GAL_PIPELINES > m_pipelines
Shared by all windows and created once with the device.
void RenderGeometryInto(WINDOW_STATE &aWS, const FRAME_DRAW_DESC &aDesc, RHI_FramebufferHandle aFBO, bool aLogSummary, GEOMETRY_RANGES aRanges=GEOMETRY_RANGES::ALL, uint32_t aStaticDrawMargin=0)
bool RenderWindow(WINDOW_STATE &aWS)
std::unique_ptr< RHI_COMPOSITOR > m_compositor
RHI_RenderPassHandle m_geometryDepthLoadDiscardRenderPass
SYNC_QUEUE< RHI_CONTROL_COMMAND > m_controlQueue
bool SubmitControl(RHI_CONTROL_COMMAND aCmd)
void drawCursor(WINDOW_STATE &aWS)
Draw the cursor into the open composite pass.
bool Start()
Block until the render thread has created the device and its pipelines.
std::atomic< int > m_loopPhase
see GetLoopPhase()
void RenderGeometry(WINDOW_STATE &aWS)
std::vector< WINDOW_STATE * > m_activeWindowOrder
std::shared_ptr< const RHI_GLYPH_ATLAS_DATA > m_glyphAtlas
void DestroyWindowFBOs(WINDOW_STATE &aWS)
void destroyWindow(WINDOW_STATE &aWS)
Free a window's targets, uniforms, swapchain and surface.
A small class to help profiling.
Definition profile.h:46
double msecs(bool aSinceLast=false)
Definition profile.h:148
Creates, uses and destroys GPU resources through one graphics API backend.
Definition rhi_device.h:66
void Release(RHI_BufferHandle &aHandle)
Destroy the resource, if there is one, and clear the handle.
Definition rhi_device.h:260
const wxChar *const traceRhi
Flag to enable debug output of the RHI rendering backend.
static std::vector< PENDING_PROPERTY > plan(const EDA_ITEM &aSource, const EDA_ITEM &aTarget, const std::set< wxString > &aEnabledKeys)
Target properties to write, paired with values read off the source.
The Cairo implementation of the graphics abstraction layer.
Definition eda_group.h:30
GRID_UBO_DATA RhiSupersampleGrid(const GRID_UBO_DATA &aGrid, uint32_t aFactor)
bool RhiViewMovedOnly(const FRAME_UBO_DATA &aDrawn, const FRAME_UBO_DATA &aNow)
RHI_CULL_VIEW RhiCullView(const FRAME_UBO_DATA &aFrame)
Invert the frame's 2D projection over the normalized device square.
std::function< std::unique_ptr< RHI_DEVICE >()> RHI_DEVICE_FACTORY
Creates the RHI_DEVICE.
std::optional< RHI_VIEW_SHIFT > RhiViewShift(const FRAME_UBO_DATA &aDrawn, const FRAME_UBO_DATA &aNow)
constexpr uint32_t RhiCullRunLength(int aShape)
void RhiForVisibleStretches(const std::vector< RHI_RUN_BOUNDS > *aRuns, uint32_t aRunLength, uint32_t aStart, uint32_t aCount, const RHI_CULL_VIEW &aView, DRAW &&aDraw, RHI_CULL_COUNT *aTally=nullptr)
Call aDraw( first, count ) for each stretch of [aStart, aStart + aCount) whose runs can reach aView.
FRAME_UBO_DATA RhiSupersampleFrame(const FRAME_UBO_DATA &aFrame, uint32_t aFactor)
RHI_SHAPE
Shapes a frame draws, each with its own array and pipelines.
@ RHI_SHAPE_POLYGON
@ RHI_SHAPE_TRACK
@ RHI_SHAPE_PAD
@ RHI_SHAPE_ARC
@ TARGET_OVERLAY
Items that may change while the view stays the same (noncached)
Definition definitions.h:35
FRAME_UBO_DATA RhiWidenFrame(const FRAME_UBO_DATA &aFrame, uint32_t aMargin)
constexpr int RHI_INSTANCED_SHAPES
bool RhiGridMovedOnly(const GRID_UBO_DATA &aDrawn, const GRID_UBO_DATA &aNow)
@ MARK
write depth only, so the layer's later draws stay out of these pixels
@ NEGATIVE
erase what the layer has drawn so far, inside a temp target
@ ONCE
blend, but let each pixel take the layer only once
@ BLEND
ordinary alpha blending
std::vector< LAYER_DRAW_STEP > RhiPlanLayerDraws(const LAYER_RANGE &aRange, const std::vector< POLARITY_RUN > &aRuns={})
Order the draws of one layer range.
GRID_UBO_DATA RhiWidenGrid(const GRID_UBO_DATA &aGrid, uint32_t aMargin)
bool SetCurrentThreadPriority(bool aRaise)
Raise or lower the scheduling priority of the calling thread.
Definition unix/app.cpp:123
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
static void drawBitmap(wxBitmap &aBitmap, wxColor aColor)
static bool ShouldRetrySwapchainCreate(const KIGFX::WINDOW_STATE &aWS, KIGFX::WINDOW_STATE::Clock::time_point aNow, std::chrono::milliseconds aBackoff=std::chrono::milliseconds(250))
static RHI_FRAMEBUFFER_DESC marginFramebufferDesc(const KIGFX::WINDOW_STATE &aWS)
static MARGIN_PLAN planMarginTarget(const KIGFX::WINDOW_STATE::RENDERED_CONTENT &aDrawn, uint64_t aEpoch, const KIGFX::FRAME_DRAW_DESC &aNow, uint32_t aMargin, bool aDrawsGrid)
static double WindowStateDurationMs(KIGFX::WINDOW_STATE::Clock::time_point aStart, KIGFX::WINDOW_STATE::Clock::time_point aEnd)
static RHI_FRAMEBUFFER_DESC layerFramebufferDesc(const KIGFX::WINDOW_STATE &aWS)
static long long WindowStateAgeMs(KIGFX::WINDOW_STATE::Clock::time_point aThen, KIGFX::WINDOW_STATE::Clock::time_point aNow)
static COMPOSITOR_LAYER marginLayer(const KIGFX::WINDOW_STATE &aWS, RHI_FramebufferHandle aFBO, const KIGFX::RHI_VIEW_SHIFT &aShift)
static void destroyDiffCopies(RHI_DEVICE &aDevice, KIGFX::WINDOW_STATE &aWS)
static uint32_t fitSupersampleFactor(uint32_t aRequested, const KIGFX::WINDOW_STATE &aWS, uint32_t aMaxTextureSize)
static uint32_t staticMarginFor(const KIGFX::WINDOW_STATE &aWS, uint32_t aMaxTextureSize)
constexpr uint32_t RHI_UBO_BINDING
MSL is generated with –msl-decoration-binding, so a Metal buffer index is its binding number in every...
RHI_BUFFER_TYPE
Definition rhi_buffer.h:26
std::variant< RHI_CMD_FRAME_REQUEST, RHI_CMD_SWAPCHAIN_CREATE, RHI_CMD_SWAPCHAIN_RESIZE, RHI_CMD_SWAPCHAIN_DESTROY, RHI_CMD_FENCE > RHI_CONTROL_COMMAND
One entry in the render thread control queue.
uint32_t RHI_WINDOW_ID
Routes control commands to a swapchain.
RHI_FRAME_OP_RESULT
Windows headers define ERROR as a macro.
Definition rhi_device.h:44
constexpr uint32_t RHI_GLYPH_TABLE_BINDING
Binding of the glyph table beside the atlas texture at 0, see RHI_UBO_BINDING.
constexpr uint32_t RHI_GLYPH_TABLE_SIZE
Length of the shaders' glyph table.
RHI_HANDLE< RHI_TAG_FRAMEBUFFER > RHI_FramebufferHandle
Definition rhi_handle.h:93
RHI_HANDLE< RHI_TAG_TEXTURE > RHI_TextureHandle
Definition rhi_handle.h:84
RHI_HANDLE< RHI_TAG_BUFFER > RHI_BufferHandle
Definition rhi_handle.h:83
RHI_HANDLE< RHI_TAG_PIPELINE > RHI_PipelineHandle
Definition rhi_handle.h:86
RHI_HANDLE< RHI_TAG_BIND_GROUP > RHI_BindGroupHandle
Definition rhi_handle.h:90
RHI_HANDLE< RHI_TAG_BIND_GROUP_LAYOUT > RHI_BindGroupLayoutHandle
Definition rhi_handle.h:89
RHI_HANDLE< RHI_TAG_RENDER_PASS > RHI_RenderPassHandle
Definition rhi_handle.h:88
TIME_POINT RhiNextFrameAt(TIME_POINT aFrameStart, double aRefreshHz, std::optional< TIME_POINT > aVblank)
The earliest time the frame after one started at aFrameStart may start, for a window whose presents d...
constexpr int RHI_PRESENTS_IN_FLIGHT
Presents allowed to wait for the display at once while frames are paced.
std::variant< RHI_GEOMETRY_UPLOAD, RHI_TEXTURE_UPLOAD, RHI_BUFFER_CREATE, RHI_TEXTURE_CREATE, RHI_BUFFER_DESTROY, RHI_TEXTURE_DESTROY > RHI_UPLOAD_COMMAND
One entry in the render thread upload queue.
One layer to composite.
Matches the PushConstants block of the compositor shader.
One textured bitmap quad with its own vertex buffer and texture.
Cursor uniform block matching the CursorUBO layout in cursor.frag (std140).
GPU geometry of one frame, built by GAL_MODERN after EndDrawing() and passed through the FRAME_MAILBO...
RHI_LINE_PIPELINE_KIND linePipelineKind
RHI_SHAPE_COUNTS negCount
FRAME_UBO_DATA frameUBO
bool gridFirst
Draw the grid before the layers instead of over them.
RHI_SHAPE_COUNTS count
bool testAgainstStatic
Slices share the static target's layer order mapping, so this target can be depth tested against a co...
std::vector< LAYER_RANGE > layerRanges
std::vector< POLARITY_RUN > polarityRuns
CURSOR_UBO_DATA cursorUBO
std::vector< BITMAP_DRAW_DESC > bitmapDraws
std::array< RHI_BufferHandle, RHI_SHAPES > buffer
Each shape's buffer, normal elements before negatives.
Per-frame uniform block matching FrameUBO in frame_ubo.glsl (std140).
Grid uniform block matching the GridUBO layout in grid.frag (std140).
One draw of a layer range.
One layer's instance ranges within the flat GPU buffers, normal instances before negatives.
std::array< RHI_BufferHandle, RHI_SHAPES > buffer
Buffers a retained scene layer owns per shape.
bool difference
A diff layer (GAL::StartDiffLayer) that replaces what is under it with the colour difference,...
bool HasGeometry() const
bool overlayAlpha
Elements flagged RHI_INSTANCE_OVERLAY_ALPHA, drawn with RHI_GAL_PIPELINES::GetPremultipliedVariant.
std::shared_ptr< const RHI_RUN_TABLE > cullRuns
Bounds of the range's buffers so off-screen runs are skipped.
std::array< RHI_BufferHandle, RHI_SHAPES > negBuffer
bool NeedsTempFBO() const
float depthNear
Depth slice the range is drawn into.
float alpha
Compositing alpha.
RHI_LAYER_DIM dim
The world rectangle a frame shows and the world size of one of its pixels.
Fade of a whole layer towards a colour, laid out as the push constants of layer_dim....
How far the view has moved since the static target was drawn, in whole frame pixels.
One window's uniforms for one frame.
Uniform buffers owned by one geometry target.
What a geometry target holds, so a frame asking for the same content at the same camera can reuse it.
uint32_t margin
how much of the static target's margin was drawn
Per-window state owned by the render thread.
RHI_VIEW_SHIFT staticShift
How far the view has moved since the static target was drawn.
RHI_FramebufferHandle diffBelowFBO
Copies of what lies under a difference layer, for the view sized targets and the static one.
RHI_FramebufferHandle staticDiffBelowFBO
RHI_FramebufferHandle dynamicFBO
Clock::time_point lastRenderStartTime
RHI_FramebufferHandle overlayGeometryFBO
Overlay content, drawn from a transparent start and composited over the dynamic target (see RHI_GAL_P...
FRAME_UNIFORMS uniformSets[UNIFORM_GENERATIONS]
Clock::time_point lastFrameRequestTime
static constexpr size_t UNIFORM_GENERATIONS
A frame's passes are still queued when the next frame is built, so frames rotate through several unif...
Clock::time_point lastSwapchainCreateAttemptTime
uint32_t supersampleFactor
Supersampling factor.
RHI_FramebufferHandle ShownStaticFBO() const
float refreshHz
A swapchain that presents immediately is drawn at most once per refresh unless the frame answers inpu...
bool staticInMargin
the content shown is in staticFBO
RHI_FramebufferHandle staticTempFBO
The static target's temp target, made when a static layer first needs one.
Clock::time_point lastPresentAttemptTime
RHI_NATIVE_WINDOW_HANDLE nativeHandle
int32_t ShownStaticX() const
Where the view starts in the shown static target, in texels.
Clock::time_point lastPresentFailureTime
RHI_FramebufferHandle accumulationFBO
Color only, without depth or stencil.
FRAME_DRAW_DESC staticDrawDesc
Static and dynamic pool descriptions, each drawn into its own target.
RHI_FramebufferHandle staticViewFBO
Static content drawn without the margin, as during a zoom, goes into this view sized target so the zo...
RHI_FramebufferHandle overlayMarginFBO
Overlay content that stayed the same through a pan, drawn with the static target's margin.
RHI_VIEW_SHIFT overlayShift
How far the view has moved since overlayMarginFBO was drawn.
uint64_t geometryRenderPasses
render passes begun while drawing geometry
uint32_t staticMargin
Frame pixels the static target holds beyond each view edge so a pan within them is only a shift.
uint64_t dynamicPasses
times dynamicFBO was re-rendered
Clock::time_point lastPresentSuccessTime
FRAME_MAILBOX * mailbox
Owned by the panel.
bool dynamicGeometryDrawn
An empty dynamicFBO is neither drawn nor composited.
RHI_FramebufferHandle staticFBO
RHI_FramebufferHandle tempFBO
uint64_t staticPasses
times staticFBO was re-rendered
RENDERED_CONTENT renderedOverlay
the overlay target keeps the static target's margin
Clock::time_point lastRenderCompleteTime
KIGFX::RHI_VIEW_SHIFT shift
uint32_t drawMargin
std::vector< RHI_BIND_GROUP_RESOURCE > entries
RHI_BindGroupLayoutHandle layout
RHI_BUFFER_TYPE type
Definition rhi_buffer.h:35
bool flushFrames
Draw the frames requested before the fence, for a reader of their pixels.
std::function< void()> onComplete
float refreshHz
display refresh rate, 0 when unknown
KIGFX::FRAME_MAILBOX * frame2DMailbox
Owned by the panel.
RHI_NATIVE_WINDOW_HANDLE nativeHandle
std::vector< RHI_TEXTURE_FORMAT > colorFormats
RHI_FramebufferHandle framebuffer
Definition rhi_command.h:49
RHI_RenderPassHandle renderPass
Definition rhi_command.h:48
std::vector< RHI_COLOR_ATTACHMENT_DESC > colorAttachments
RHI_DEPTH_STENCIL_ATTACHMENT_DESC depthStencilAttachment
Same pixel space as RHI_VIEWPORT.
Definition rhi_command.h:39
Descriptor for creating a surface from a native window.
Definition rhi_surface.h:71
RHI_NATIVE_WINDOW_HANDLE window
Definition rhi_surface.h:72
Descriptor for creating a swapchain.
Definition rhi_surface.h:79
RHI_SurfaceHandle surface
Definition rhi_surface.h:80
RHI_TEXTURE_FORMAT format
Definition rhi_texture.h:40
RHI_FILTER filter
Definition rhi_texture.h:41
Position is in world coordinates, color is premultiplied alpha and u, v hold half floats.
Definition rhi_vertex.h:37
void SetPosition(float aX, float aY)
Definition rhi_vertex.h:47
void SetColor(uint8_t aR, uint8_t aG, uint8_t aB, uint8_t aA=255)
Definition rhi_vertex.h:48
void SetUV(float aU, float aV)
Definition rhi_vertex.h:52
Top-left origin, in device pixels.
Definition rhi_command.h:28
bool copied
wxLogTrace helper definitions.