102 Patches: Detours to the Rescue
C reference for DttR maintainers and modders.
Loading...
Searching...
No Matches
backend_sdl3gpu.c
Go to the documentation of this file.
2#include "graphics_private.h"
3
4#include <dttr_log.h>
5
6#include <dttr_config.h>
7
8#define DRIVER_DISPLAY_VULKAN "Vulkan"
9#define DRIVER_DISPLAY_DIRECT3D12 "Direct3D 12"
10
11#ifdef DTTR_MODS_ENABLED
14#endif
15
16#include <math.h>
17#include <stdlib.h>
18#include <string.h>
19
21static void cleanup(DTTR_BackendState *state);
22
23static SDL_GPUSampleCount msaa_sample_count_from_config(int value) {
24 switch (value) {
25 case 2:
26 return SDL_GPU_SAMPLECOUNT_2;
27 case 4:
28 return SDL_GPU_SAMPLECOUNT_4;
29 case 8:
30 return SDL_GPU_SAMPLECOUNT_8;
31 default:
32 return SDL_GPU_SAMPLECOUNT_1;
33 }
34}
35
36static int msaa_sample_count_to_int(SDL_GPUSampleCount value) {
37 switch (value) {
38 case SDL_GPU_SAMPLECOUNT_2:
39 return 2;
40 case SDL_GPU_SAMPLECOUNT_4:
41 return 4;
42 case SDL_GPU_SAMPLECOUNT_8:
43 return 8;
44 default:
45 return 1;
46 }
47}
48
49// Uses the requested MSAA count only when both swapchain and depth formats support it.
51 const SDL_GPUSampleCount requested = msaa_sample_count_from_config(
52 dttr_config.msaa_samples
53 );
54 if (requested == SDL_GPU_SAMPLECOUNT_1) {
55 return SDL_GPU_SAMPLECOUNT_1;
56 }
57
58 const SDL_GPUTextureFormat swapchain_fmt = SDL_GetGPUSwapchainTextureFormat(
59 state->device,
60 state->window
61 );
62 const bool color_supported = SDL_GPUTextureSupportsSampleCount(
63 state->device,
64 swapchain_fmt,
65 requested
66 );
67 const bool depth_supported = SDL_GPUTextureSupportsSampleCount(
68 state->device,
69 SDL_GPU_TEXTUREFORMAT_D32_FLOAT,
70 requested
71 );
72
73 if (color_supported && depth_supported) {
74 return requested;
75 }
76
78 "Requested MSAA x%d is unsupported on this device/format. "
79 "Falling back to x1.",
81 );
82 return SDL_GPU_SAMPLECOUNT_1;
83}
84
86 if (!state->device) {
87 return;
88 }
89
90 SDL_DestroyGPUDevice(state->device);
91 state->device = NULL;
92}
93
95 if (!state->device) {
96 return;
97 }
98
99 SDL_ReleaseWindowFromGPUDevice(state->device, state->window);
101}
102
103// Attempts one SDL GPU driver and only keeps it after the game window can be claimed.
106 const SDL_GPUShaderFormat requested_formats,
107 const char *driver
108) {
109 state->device = SDL_CreateGPUDevice(requested_formats, false, driver);
110
111 if (!state->device) {
113 "Failed to create SDL GPU device for driver '%s' "
114 "(requested_formats=0x%x): %s",
115 driver ? driver : "default",
116 (unsigned int)requested_formats,
117 SDL_GetError()
118 );
119 return false;
120 }
121
122 if (!SDL_ClaimWindowForGPUDevice(state->device, state->window)) {
124 "Failed to claim window for SDL GPU driver '%s': %s",
125 driver ? driver : "default",
126 SDL_GetError()
127 );
129 return false;
130 }
131
132 const bool immediate_ok = SDL_WindowSupportsGPUPresentMode(
133 state->device,
134 state->window,
135 SDL_GPU_PRESENTMODE_IMMEDIATE
136 );
137 if (!immediate_ok) {
139 "IMMEDIATE present mode unsupported for '%s', falling back to VSYNC",
140 driver ? driver : "default"
141 );
142 }
143
144 if (!SDL_SetGPUSwapchainParameters(
145 state->device,
146 state->window,
147 SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
148 immediate_ok ? SDL_GPU_PRESENTMODE_IMMEDIATE : SDL_GPU_PRESENTMODE_VSYNC
149 )) {
150 DTTR_LOG_ERROR("Failed to set swap chain parameters: %s", SDL_GetError());
151 }
152
153 const SDL_GPUShaderFormat available_formats = SDL_GetGPUShaderFormats(state->device);
154 const char *active_driver = SDL_GetGPUDeviceDriver(state->device);
156 active_driver,
157 available_formats
158 );
159
160 if (state->shader_format != SDL_GPU_SHADERFORMAT_INVALID) {
161 return true;
162 }
163
165 "SDL GPU driver '%s' does not support required shader format. Available "
166 "mask=0x%x",
167 active_driver ? active_driver : "unknown",
168 (unsigned int)available_formats
169 );
171 return false;
172}
173
174// Maps a configured graphics API to the SDL GPU driver name requested at device creation.
176 switch (api) {
178 return DTTR_DRIVER_VULKAN;
181 default:
182 return NULL;
183 }
184}
185
186// Creates an SDL GPU device using the configured driver or the supported fallback order.
188 const SDL_GPUShaderFormat requested_formats = dttr_graphics_requested_shader_formats();
189 const char *const requested_driver = graphics_api_driver_name(
190 dttr_config.graphics_api
191 );
192
193 if (requested_driver) {
194 if (try_create_device_for_driver(state, requested_formats, requested_driver)) {
195 return true;
196 }
197
199 "GPU device creation failed for configured graphics_api='%s'; no fallback "
200 "APIs "
201 "will be attempted",
202 requested_driver
203 );
204 return false;
205 }
206
207 const char *const driver_candidates[] = {
210 NULL, // Falls back to the SDL default driver selection.
211 };
212
213 for (size_t i = 0; i < SDL_arraysize(driver_candidates); i++) {
214 if (try_create_device_for_driver(state, requested_formats, driver_candidates[i])) {
215 return true;
216 }
217 }
218
219 DTTR_LOG_ERROR("GPU device creation failed for all supported APIs (d3d12/vulkan)");
220 return false;
221}
222
223// Initializes SDL GPU backend state after device creation succeeds.
225 if (!create_device(state)) {
226 return false;
227 }
228
229 sdl3_gpu_backend_data *bd = calloc(1, sizeof(sdl3_gpu_backend_data));
230 if (!bd) {
232 return false;
233 }
234
235 state->backend_data = bd;
236 state->backend_type = DTTR_BACKEND_SDL_GPU;
237 state->renderer = &renderer;
238
239 state->msaa_sample_count = select_msaa_sample_count(state);
241 "MSAA requested: x%d, effective: x%d",
242 dttr_config.msaa_samples,
243 msaa_sample_count_to_int(state->msaa_sample_count)
244 );
245
247 "SDL GPU initialized with %s (shaders: %s)",
248 SDL_GetGPUDeviceDriver(state->device),
250 );
251
254 DTTR_LOG_ERROR("Failed to create GPU resources");
255 cleanup(state);
256 state->renderer = NULL;
257 return false;
258 }
259
260 return true;
261}
262
263// Releases all SDL GPU resources owned by the backend before the window/device go away.
265 if (!state->device) {
266 return;
267 }
268
269 for (int i = 0; i < DTTR_SAMPLER_COUNT; i++) {
270 if (state->samplers[i]) {
271 SDL_ReleaseGPUSampler(state->device, state->samplers[i]);
272 }
273 }
274
275 if (state->dummy_texture) {
276 SDL_ReleaseGPUTexture(state->device, state->dummy_texture);
277 }
278
279 if (state->depth_texture) {
280 SDL_ReleaseGPUTexture(state->device, state->depth_texture);
281 }
282
283 if (state->msaa_render_target) {
284 SDL_ReleaseGPUTexture(state->device, state->msaa_render_target);
285 }
286
287 if (state->render_target) {
288 SDL_ReleaseGPUTexture(state->device, state->render_target);
289 }
290
291 if (state->transfer_buffer) {
292 SDL_ReleaseGPUTransferBuffer(state->device, state->transfer_buffer);
293 }
294
295 if (state->vertex_buffer) {
296 SDL_ReleaseGPUBuffer(state->device, state->vertex_buffer);
297 }
298
299 for (int i = 0; i < DTTR_UPLOAD_POOL_SIZE; i++) {
300 DTTR_UploadPoolSlot *slot = &state->upload_pool[i];
301
302 if (slot->transfer_buffer) {
303 SDL_ReleaseGPUTransferBuffer(state->device, slot->transfer_buffer);
304 slot->transfer_buffer = NULL;
305 }
306
307 slot->capacity = 0;
308 slot->in_use = false;
309 }
310
311 for (int i = 0; i < DTTR_PIPELINE_COUNT; i++) {
312 if (state->pipelines[i]) {
313 SDL_ReleaseGPUGraphicsPipeline(state->device, state->pipelines[i]);
314 }
315 }
316
318 free(state->backend_data);
319 state->backend_data = NULL;
320}
321
322typedef struct {
323 SDL_GPUTexture *tex;
324 uint32_t bytes;
328
329typedef struct {
330 uint32_t draw_count;
331 uint32_t clear_count;
335
336typedef struct {
338 SDL_GPUTexture *last_texture;
339 SDL_GPUSampler *last_sampler;
341
343 return state->msaa_sample_count != SDL_GPU_SAMPLECOUNT_1
344 && state->msaa_render_target != NULL;
345}
346
347static SDL_GPUTransferBuffer *create_upload_buffer(
349 uint32_t bytes
350) {
351 const SDL_GPUTransferBufferCreateInfo info = {
352 .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
353 .size = bytes,
354 };
355
356 return SDL_CreateGPUTransferBuffer(state->device, &info);
357}
358
360 if (!state || pool_slot < 0 || pool_slot >= DTTR_UPLOAD_POOL_SIZE) {
361 return;
362 }
363
364 state->upload_pool[pool_slot].in_use = false;
365}
366
367// Reuses or grows a transfer-buffer slot large enough for the pending texture upload.
369 if (!state || !state->device || bytes == 0) {
370 return -1;
371 }
372
373 int free_slot = -1;
374 int grow_slot = -1;
375
376 for (int i = 0; i < DTTR_UPLOAD_POOL_SIZE; i++) {
377 DTTR_UploadPoolSlot *slot = &state->upload_pool[i];
378
379 if (slot->in_use) {
380 continue;
381 }
382
383 if (slot->transfer_buffer && slot->capacity >= bytes) {
384 slot->in_use = true;
385 return i;
386 }
387
388 if (free_slot < 0) {
389 free_slot = i;
390 }
391
392 if (slot->transfer_buffer) {
393 grow_slot = i;
394 }
395 }
396
397 const int slot_index = (grow_slot >= 0) ? grow_slot : free_slot;
398
399 if (slot_index < 0) {
400 return -1;
401 }
402
403 DTTR_UploadPoolSlot *slot = &state->upload_pool[slot_index];
404
405 if (slot->transfer_buffer) {
406 SDL_ReleaseGPUTransferBuffer(state->device, slot->transfer_buffer);
407 slot->transfer_buffer = NULL;
408 }
409
411
412 if (!slot->transfer_buffer) {
413 slot->capacity = 0;
414 slot->in_use = false;
415 return -1;
416 }
417
418 slot->capacity = bytes;
419 slot->in_use = true;
420 return slot_index;
421}
422
425 SDL_GPURenderPass *render_pass
426) {
427 if (!render_pass) {
428 return;
429 }
430
431 const SDL_GPUBufferBinding vbuf_binding = {
432 .buffer = state->vertex_buffer,
433 };
434
435 SDL_BindGPUVertexBuffers(render_pass, 0, &vbuf_binding, 1);
436}
437
439 if (!state->render_pass) {
440 return;
441 }
442
443 SDL_EndGPURenderPass(state->render_pass);
444 state->render_pass = NULL;
445}
446
447// Frees textures queued from non-render threads once the GPU thread reaches a safe point.
450 if (!bd) {
451 return;
452 }
453
454 SDL_LockMutex(state->texture_mutex);
455
456 for (int i = 0; i < bd->deferred_destroy_count; i++) {
457 SDL_ReleaseGPUTexture(state->device, bd->deferred_destroys[i]);
458 }
459
461 SDL_UnlockMutex(state->texture_mutex);
462}
463
464// Queues a staged texture for GPU-thread destruction instead of freeing it from callers.
465static void defer_texture_destroy(DTTR_BackendState *state, int texture_index) {
467 if (!bd || texture_index < 0 || texture_index >= DTTR_MAX_STAGED_TEXTURES) {
468 return;
469 }
470
471 DTTR_StagedTexture *st = &state->staged_textures[texture_index];
472 if (st->gpu_tex && state->device
474 bd->deferred_destroys[bd->deferred_destroy_count++] = st->gpu_tex;
475 }
476}
477
478// Copies one detached pixel buffer into a GPU texture using either the upload pool or a
479// temporary transfer buffer.
482 SDL_GPUCopyPass *copy,
483 SDL_GPUTexture *tex,
484 void *pixels,
485 int width,
486 int height,
487 uint32_t bytes
488) {
489 if (!copy || !tex || !pixels || bytes == 0) {
490 free(pixels);
491 return false;
492 }
493
494 SDL_GPUTransferBuffer *tbuf = NULL;
495 bool from_pool = false;
496 int pool_slot = -1;
497
498 if (dttr_config.texture_upload_sync) {
499 pool_slot = acquire_upload_pool_slot(state, bytes);
500 }
501
502 if (pool_slot >= 0) {
503 tbuf = state->upload_pool[pool_slot].transfer_buffer;
504 from_pool = true;
505 } else {
506 tbuf = create_upload_buffer(state, bytes);
507
508 if (!tbuf) {
509 free(pixels);
510 return false;
511 }
512 }
513
514 void *mapped = SDL_MapGPUTransferBuffer(state->device, tbuf, false);
515
516 if (!mapped) {
517 if (from_pool) {
519 } else {
520 SDL_ReleaseGPUTransferBuffer(state->device, tbuf);
521 }
522
523 free(pixels);
524 return false;
525 }
526
527 memcpy(mapped, pixels, bytes);
528 SDL_UnmapGPUTransferBuffer(state->device, tbuf);
529
530 const SDL_GPUTextureTransferInfo src = {
531 .transfer_buffer = tbuf,
532 .pixels_per_row = (Uint32)width,
533 };
534
535 const SDL_GPUTextureRegion dst = {
536 .texture = tex,
537 .w = (Uint32)width,
538 .h = (Uint32)height,
539 .d = 1,
540 };
541
542 SDL_UploadToGPUTexture(copy, &src, &dst, false);
543
544 if (from_pool) {
546 } else {
547 SDL_ReleaseGPUTransferBuffer(state->device, tbuf);
548 }
549
550 free(pixels);
551 return true;
552}
553
554// Detaches queued texture uploads under the mutex, uploads them, and keeps failed
555// entries queued for retry.
558 SDL_GPUCopyPass *copy,
559 graphics_pending_upload *pending_uploads,
560 int max_uploads
561) {
562 if (!state->texture_mutex) {
563 return 0;
564 }
565
566 int pending_count = 0;
567 SDL_LockMutex(state->texture_mutex);
568 const size_t queued_count = kv_size(state->pending_upload_indices);
569 size_t deferred_write = 0;
570
571 typedef struct {
572 SDL_GPUTexture *tex;
573 void *pixels;
574 int width;
575 int height;
576 uint32_t bytes;
577 bool generate_mips;
578 } detached_upload;
579
580 detached_upload detached[DTTR_MAX_STAGED_TEXTURES];
581
582 for (size_t q = 0; q < queued_count; q++) {
583 const int idx = kv_A(state->pending_upload_indices, q);
584
585 if (idx < 0 || idx >= state->staged_texture_count) {
586 continue;
587 }
588
589 DTTR_StagedTexture *st = &state->staged_textures[idx];
590
591 if (!st->pixels) {
592 st->pending_upload = false;
593 continue;
594 }
595
596 if (max_uploads > 0 && pending_count >= max_uploads) {
597 kv_A(state->pending_upload_indices, deferred_write++) = idx;
598 continue;
599 }
600
601 st->pending_upload = false;
602
604 free(st->pixels);
605 st->pixels = NULL;
606 continue;
607 }
608
609 const uint32_t bytes = (uint32_t)(st->width * st->height * 4);
610
611 if (bytes == 0) {
612 free(st->pixels);
613 st->pixels = NULL;
614 continue;
615 }
616
617 if (pending_count >= DTTR_MAX_STAGED_TEXTURES) {
618 free(st->pixels);
619 st->pixels = NULL;
620 continue;
621 }
622
623 detached[pending_count] = (detached_upload){
624 .tex = st->gpu_tex,
625 .pixels = st->pixels,
626 .width = st->width,
627 .height = st->height,
628 .bytes = bytes,
629 .generate_mips = dttr_config.generate_texture_mipmaps,
630 };
631
632 st->pixels = NULL;
633 pending_count++;
634 }
635
636 state->pending_upload_indices.n = deferred_write;
637 SDL_UnlockMutex(state->texture_mutex);
638
639 for (int i = 0; i < pending_count; i++) {
640 const bool ok = upload_texture_data(
641 state,
642 copy,
643 detached[i].tex,
644 detached[i].pixels,
645 detached[i].width,
646 detached[i].height,
647 detached[i].bytes
648 );
649 pending_uploads[i] = (graphics_pending_upload){
650 .tex = detached[i].tex,
651 .bytes = detached[i].bytes,
652 .generate_mips = detached[i].generate_mips,
653 .uploaded = ok,
654 };
655 }
656
657 return pending_count;
658}
659
660// Generates mipmaps for uploaded textures that requested them and records upload stats.
663 SDL_GPUCommandBuffer *cmd,
664 const graphics_pending_upload *pending,
665 int pending_count,
666 uint32_t *uploaded_texture_count,
667 uint64_t *uploaded_bytes
668) {
669 for (int p = 0; p < pending_count; p++) {
670 if (!pending[p].uploaded || !pending[p].tex) {
671 continue;
672 }
673
674 if (pending[p].generate_mips) {
675 SDL_GenerateMipmapsForGPUTexture(cmd, pending[p].tex);
676 state->perf_mips_generated_accum++;
677 } else {
678 state->perf_mips_skipped_accum++;
679 }
680
681 if (uploaded_texture_count) {
682 (*uploaded_texture_count)++;
683 }
684
685 if (uploaded_bytes) {
686 (*uploaded_bytes) += pending[p].bytes;
687 }
688 }
689}
690
691// Runs the pending texture upload copy pass and updates per-frame upload counters.
692static void upload_pending_textures(DTTR_BackendState *state, SDL_GPUCommandBuffer *cmd) {
693 if (!cmd) {
694 return;
695 }
696
698
699 SDL_GPUCopyPass *copy = SDL_BeginGPUCopyPass(cmd);
700 const int pending_count = collect_and_upload_pending(state, copy, pending, 0);
701
702 if (copy) {
703 SDL_EndGPUCopyPass(copy);
704 }
705
706 if (pending_count == 0) {
707 return;
708 }
709
710 uint32_t uploaded_texture_count = 0;
711 uint64_t uploaded_bytes = 0;
713 state,
714 cmd,
715 pending,
716 pending_count,
717 &uploaded_texture_count,
718 &uploaded_bytes
719 );
720
721 state->perf_upload_textures_accum += uploaded_texture_count;
722 state->perf_upload_bytes_accum += uploaded_bytes;
723}
724
726 if (!state->render_pass) {
727 return;
728 }
729
730 const SDL_GPUViewport viewport = {
731 .x = 0.0f,
732 .y = 0.0f,
733 .w = (float)state->width,
734 .h = (float)state->height,
735 .min_depth = 0.0f,
736 .max_depth = 1.0f,
737 };
738
739 SDL_SetGPUViewport(state->render_pass, &viewport);
740
741 const SDL_Rect scissor = {
742 .x = 0,
743 .y = 0,
744 .w = state->width,
745 .h = state->height,
746 };
747
748 SDL_SetGPUScissor(state->render_pass, &scissor);
749}
750
751// Opens a draw render pass lazily so queued clear and draw records can share command
752// buffers.
754 if (state->render_pass) {
755 return false;
756 }
757
758 const bool use_msaa = msaa_enabled(state);
759 const SDL_GPUColorTargetInfo color_target = {
760 .texture = use_msaa ? state->msaa_render_target : state->render_target,
761 .load_op = SDL_GPU_LOADOP_LOAD,
762 .store_op = use_msaa ? SDL_GPU_STOREOP_RESOLVE_AND_STORE : SDL_GPU_STOREOP_STORE,
763 .resolve_texture = use_msaa ? state->render_target : NULL,
764 .resolve_mip_level = 0,
765 .resolve_layer = 0,
766 };
767
768 const SDL_GPUDepthStencilTargetInfo depth_target = {
769 .texture = state->depth_texture,
770 .load_op = SDL_GPU_LOADOP_LOAD,
771 .store_op = SDL_GPU_STOREOP_DONT_CARE,
772 };
773
774 state->render_pass = SDL_BeginGPURenderPass(
775 state->cmd,
776 &color_target,
777 1,
778 &depth_target
779 );
780
781 if (!state->render_pass) {
782 DTTR_LOG_WARN("Failed to begin render pass");
783 return false;
784 }
785
786 bind_frame_vertex_buffer(state, state->render_pass);
788 return true;
789}
790
791static void reset_replay_state(graphics_replay_state *replay_state) {
792 if (!replay_state) {
793 return;
794 }
795
796 replay_state->last_pipeline_idx = -1;
797 replay_state->last_texture = NULL;
798 replay_state->last_sampler = NULL;
799}
800
801// Starts a render pass configured for the clear flags recorded by the DirectDraw replay
802// layer.
805 const DTTR_BatchRecord *rec,
806 graphics_replay_state *replay_state
807) {
809
810 const bool use_msaa = msaa_enabled(state);
811 const SDL_GPUColorTargetInfo color_target = {
812 .texture = use_msaa ? state->msaa_render_target : state->render_target,
813 .clear_color = rec->clear.color,
814 .load_op = (rec->clear.flags & DTTR_CLEAR_COLOR) ? SDL_GPU_LOADOP_CLEAR
815 : SDL_GPU_LOADOP_LOAD,
816 .store_op = use_msaa ? SDL_GPU_STOREOP_RESOLVE_AND_STORE : SDL_GPU_STOREOP_STORE,
817 .resolve_texture = use_msaa ? state->render_target : NULL,
818 .resolve_mip_level = 0,
819 .resolve_layer = 0,
820 };
821
822 const SDL_GPUDepthStencilTargetInfo depth_target = {
823 .texture = state->depth_texture,
824 .clear_depth = rec->clear.depth,
825 .load_op = (rec->clear.flags & DTTR_CLEAR_DEPTH) ? SDL_GPU_LOADOP_CLEAR
826 : SDL_GPU_LOADOP_LOAD,
827 .store_op = SDL_GPU_STOREOP_STORE,
828 };
829
830 state->render_pass = SDL_BeginGPURenderPass(
831 state->cmd,
832 &color_target,
833 1,
834 &depth_target
835 );
836 bind_frame_vertex_buffer(state, state->render_pass);
838 reset_replay_state(replay_state);
839}
840
841// Replays one recorded draw call while avoiding redundant pipeline and sampler binds.
844 const DTTR_BatchRecord *rec,
845 graphics_replay_state *replay_state,
846 graphics_replay_stats *replay_stats
847) {
848 const bool began_pass = begin_draw_pass_if_needed(state);
849
850 if (began_pass) {
851 reset_replay_state(replay_state);
852 }
853
854 if (!state->render_pass) {
855 return;
856 }
857
858 const int pidx = DTTR_PIPELINE_INDEX(
859 rec->draw.blend_mode,
860 rec->draw.depth_test,
861 rec->draw.depth_write
862 );
863
864 if (!replay_state || replay_state->last_pipeline_idx != pidx) {
865 SDL_BindGPUGraphicsPipeline(state->render_pass, state->pipelines[pidx]);
866
867 if (replay_state) {
868 replay_state->last_pipeline_idx = pidx;
869 }
870
871 if (replay_stats) {
872 replay_stats->pipeline_bind_count++;
873 }
874 }
875
876 SDL_PushGPUVertexUniformData(
877 state->cmd,
878 0,
879 &rec->draw.uniforms,
880 sizeof(DTTR_Uniforms)
881 );
882 SDL_PushGPUFragmentUniformData(
883 state->cmd,
884 0,
885 &rec->draw.uniforms,
886 sizeof(DTTR_Uniforms)
887 );
888
889 if (!replay_state || replay_state->last_texture != rec->draw.texture
890 || replay_state->last_sampler != rec->draw.sampler) {
891 const SDL_GPUTextureSamplerBinding tex_binding = {
892 .texture = rec->draw.texture,
893 .sampler = rec->draw.sampler,
894 };
895
896 SDL_BindGPUFragmentSamplers(state->render_pass, 0, &tex_binding, 1);
897
898 if (replay_state) {
899 replay_state->last_texture = rec->draw.texture;
900 replay_state->last_sampler = rec->draw.sampler;
901 }
902
903 if (replay_stats) {
904 replay_stats->sampler_bind_count++;
905 }
906 }
907
908 SDL_DrawGPUPrimitives(
909 state->render_pass,
910 rec->draw.vertex_count,
911 1,
912 rec->draw.first_vertex,
913 0
914 );
915
916 if (replay_stats) {
917 replay_stats->draw_count++;
918 }
919}
920
921// Replays queued clear and draw records into SDL GPU commands for the current frame.
923 graphics_replay_stats replay_stats = {0};
924
925 if (kv_size(state->batch_records) == 0) {
926 return replay_stats;
927 }
928
929 graphics_replay_state replay_state = {0};
930 reset_replay_state(&replay_state);
931 state->render_pass = NULL;
932
933 for (size_t i = 0; i < kv_size(state->batch_records); i++) {
934 const DTTR_BatchRecord *rec = &kv_A(state->batch_records, i);
935
936 if (rec->type == DTTR_BATCH_CLEAR) {
937 begin_clear_pass(state, rec, &replay_state);
938 replay_stats.clear_count++;
939 continue;
940 }
941
942 draw_batch_record(state, rec, &replay_state, &replay_stats);
943 }
944
946 return replay_stats;
947}
948
949// Acquires the frame command buffer and swapchain texture before uploads and replay work.
951 if (!state->device || !state->window || !dttr_graphics_is_gpu_thread()) {
952 return;
953 }
954
955 state->frame_index++;
956
957 state->cmd = SDL_AcquireGPUCommandBuffer(state->device);
958
959 if (!state->cmd) {
960 DTTR_LOG_ERROR("Failed to acquire GPU command buffer");
961 return;
962 }
963
965
966 if (!SDL_WaitAndAcquireGPUSwapchainTexture(
967 state->cmd,
968 state->window,
969 &state->swapchain_tex,
970 &state->swapchain_width,
971 &state->swapchain_height
972 )) {
973 DTTR_LOG_WARN("Failed to acquire swapchain texture: %s", SDL_GetError());
974 SDL_CancelGPUCommandBuffer(state->cmd);
975 state->cmd = NULL;
976 return;
977 }
978
979 // No swapchain image available, skip this frame.
980 if (!state->swapchain_tex) {
981 SDL_CancelGPUCommandBuffer(state->cmd);
982 state->cmd = NULL;
983 return;
984 }
985
986 // Textures must be uploaded after swapchain acquire for Vulkan.
988
989 state->batch_records.n = 0;
990 state->vertex_offset = 0;
991 state->transfer_mapped = SDL_MapGPUTransferBuffer(
992 state->device,
993 state->transfer_buffer,
994 true
995 );
996
997 if (!state->transfer_mapped) {
998 DTTR_LOG_WARN("BeginFrame: MapGPUTransferBuffer failed");
999 }
1000
1001 state->frame_active = true;
1003}
1004
1005// Uploads vertices, replays draw records, blits to the swapchain, and submits the frame.
1007 state->frame_active = false;
1008
1010
1011 if (state->transfer_mapped) {
1012 SDL_UnmapGPUTransferBuffer(state->device, state->transfer_buffer);
1013 state->transfer_mapped = NULL;
1014 }
1015
1016 if (!state->cmd) {
1017 return;
1018 }
1019
1020 if (state->vertex_offset > 0) {
1021 SDL_GPUCopyPass *copy = SDL_BeginGPUCopyPass(state->cmd);
1022
1023 if (copy) {
1024 const SDL_GPUTransferBufferLocation src = {
1025 .transfer_buffer = state->transfer_buffer,
1026 };
1027
1028 const SDL_GPUBufferRegion dst = {
1029 .buffer = state->vertex_buffer,
1030 .size = state->vertex_offset * DTTR_VERTEX_SIZE,
1031 };
1032
1033 SDL_UploadToGPUBuffer(copy, &src, &dst, true);
1034 SDL_EndGPUCopyPass(copy);
1035 }
1036 }
1037
1038 const graphics_replay_stats replay_stats = replay_batch_records(state);
1039 state->perf_draws_accum += replay_stats.draw_count;
1040 state->perf_clears_accum += replay_stats.clear_count;
1041 state->perf_pipeline_binds_accum += replay_stats.pipeline_bind_count;
1042 state->perf_sampler_binds_accum += replay_stats.sampler_bind_count;
1043
1044#ifdef DTTR_MODS_ENABLED
1046 state->cmd,
1047 state->render_target,
1048 (uint32_t)state->width,
1049 (uint32_t)state->height
1050 );
1051#endif
1053
1054 DTTR_PresentRect present = {
1055 .x = 0,
1056 .y = 0,
1057 .w = state->width,
1058 .h = state->height,
1059 };
1060
1061 bool overlay_rendered = false;
1062 if (state->swapchain_tex) {
1063 const Uint32 swap_w = (state->swapchain_width > 0) ? state->swapchain_width
1064 : (Uint32)state->width;
1065 const Uint32 swap_h = (state->swapchain_height > 0) ? state->swapchain_height
1066 : (Uint32)state->height;
1067 const bool
1068 is_internal_method = (dttr_config.scaling_method == DTTR_SCALING_METHOD_LOGICAL);
1070 (int)swap_w,
1071 (int)swap_h,
1072 state->width,
1073 state->height,
1075 (!is_internal_method)
1076 && (dttr_config.scaling_fit == DTTR_SCALING_MODE_INTEGER),
1077 1.0f
1078 );
1079 overlay_rendered = true;
1080
1081 const SDL_GPUBlitInfo blit = {
1082 .source =
1083 {
1084 .texture = state->render_target,
1085 .w = state->width,
1086 .h = state->height,
1087 },
1088 .destination =
1089 {
1090 .texture = state->swapchain_tex,
1091 .x = present.x,
1092 .y = present.y,
1093 .w = present.w,
1094 .h = present.h,
1095 },
1096 .clear_color = {0.0f, 0.0f, 0.0f, 1.0f},
1097 .load_op = SDL_GPU_LOADOP_CLEAR,
1098 .filter = dttr_config.present_filter,
1099 };
1100
1101 SDL_BlitGPUTexture(state->cmd, &blit);
1102
1103#ifdef DTTR_MODS_ENABLED
1105 state->cmd,
1106 state->swapchain_tex,
1107 swap_w,
1108 swap_h,
1109 present.x,
1110 present.y,
1111 present.w,
1112 present.h
1113 );
1114#endif
1116 }
1117
1118 SDL_SubmitGPUCommandBuffer(state->cmd);
1119
1120 if (dttr_config.texture_upload_sync) {
1121 SDL_WaitForGPUIdle(state->device);
1122 }
1123
1124 dttr_graphics_mod_present_rect_after(state, &present, overlay_rendered);
1126 state->cmd = NULL;
1127}
1128
1129// Recreates the movie texture only when decoded frame dimensions change.
1130static bool ensure_video_texture(DTTR_BackendState *state, int width, int height) {
1131 if (state->video_texture && state->video_width == width
1132 && state->video_height == height) {
1133 return true;
1134 }
1135
1136 if (state->video_texture) {
1137 SDL_ReleaseGPUTexture(state->device, state->video_texture);
1138 state->video_texture = NULL;
1139 }
1140
1141 const SDL_GPUTextureCreateInfo tex_info = {
1142 .type = SDL_GPU_TEXTURETYPE_2D,
1143 .format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM,
1144 .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
1145 .width = width,
1146 .height = height,
1147 .layer_count_or_depth = 1,
1148 .num_levels = 1,
1149 .sample_count = SDL_GPU_SAMPLECOUNT_1,
1150 };
1151
1152 state->video_texture = SDL_CreateGPUTexture(state->device, &tex_info);
1153
1154 if (!state->video_texture) {
1155 return false;
1156 }
1157
1158 state->video_width = width;
1159 state->video_height = height;
1160 return true;
1161}
1162
1163// Uploads a BGRA movie frame and presents it directly while normal frame rendering is
1164// idle.
1167 const uint8_t *pixels,
1168 int width,
1169 int height,
1170 int stride
1171) {
1172 if (!state->device || !state->window || !dttr_graphics_is_gpu_thread()) {
1173 return false;
1174 }
1175
1176 if (state->frame_active) {
1177 // Video presentation assumes sole ownership of the command buffer.
1178 return false;
1179 }
1180
1181 if (!ensure_video_texture(state, width, height)) {
1182 return false;
1183 }
1184
1185 const Uint32 upload_size = (Uint32)(stride * height);
1186 SDL_GPUTransferBuffer *tbuf = create_upload_buffer(state, upload_size);
1187
1188 if (!tbuf) {
1189 return false;
1190 }
1191
1192 void *mapped = SDL_MapGPUTransferBuffer(state->device, tbuf, false);
1193
1194 if (!mapped) {
1195 SDL_ReleaseGPUTransferBuffer(state->device, tbuf);
1196 return false;
1197 }
1198
1199 memcpy(mapped, pixels, upload_size);
1200 SDL_UnmapGPUTransferBuffer(state->device, tbuf);
1201
1202 SDL_GPUCommandBuffer *cmd = SDL_AcquireGPUCommandBuffer(state->device);
1203
1204 if (!cmd) {
1205 SDL_ReleaseGPUTransferBuffer(state->device, tbuf);
1206 return false;
1207 }
1208
1209 SDL_GPUTexture *swapchain_tex = NULL;
1210 Uint32 swapchain_w = 0;
1211 Uint32 swapchain_h = 0;
1212 SDL_WaitAndAcquireGPUSwapchainTexture(
1213 cmd,
1214 state->window,
1215 &swapchain_tex,
1216 &swapchain_w,
1217 &swapchain_h
1218 );
1219
1220 SDL_GPUCopyPass *copy = SDL_BeginGPUCopyPass(cmd);
1221
1222 if (copy) {
1223 const SDL_GPUTextureTransferInfo src = {
1224 .transfer_buffer = tbuf,
1225 .offset = 0,
1226 .pixels_per_row = (Uint32)(stride / 4),
1227 .rows_per_layer = (Uint32)height,
1228 };
1229
1230 const SDL_GPUTextureRegion dst = {
1231 .texture = state->video_texture,
1232 .mip_level = 0,
1233 .layer = 0,
1234 .x = 0,
1235 .y = 0,
1236 .z = 0,
1237 .w = (Uint32)width,
1238 .h = (Uint32)height,
1239 .d = 1,
1240 };
1241
1242 SDL_UploadToGPUTexture(copy, &src, &dst, false);
1243 SDL_EndGPUCopyPass(copy);
1244 }
1245
1246 if (swapchain_tex) {
1248 (int)swapchain_w,
1249 (int)swapchain_h,
1250 width,
1251 height,
1252 false,
1253 false,
1254 1.0f
1255 );
1256
1257 const SDL_GPUBlitInfo blit = {
1258 .source =
1259 {
1260 .texture = state->video_texture,
1261 .mip_level = 0,
1262 .layer_or_depth_plane = 0,
1263 .x = 0,
1264 .y = 0,
1265 .w = (Uint32)width,
1266 .h = (Uint32)height,
1267 },
1268 .destination =
1269 {
1270 .texture = swapchain_tex,
1271 .mip_level = 0,
1272 .layer_or_depth_plane = 0,
1273 .x = present.x,
1274 .y = present.y,
1275 .w = present.w,
1276 .h = present.h,
1277 },
1278 .load_op = SDL_GPU_LOADOP_CLEAR,
1279 .clear_color = (SDL_FColor){0.0f, 0.0f, 0.0f, 1.0f},
1280 .flip_mode = SDL_FLIP_NONE,
1281 .filter = dttr_config.present_filter,
1282 .cycle = false,
1283 };
1284
1285 SDL_BlitGPUTexture(cmd, &blit);
1286 }
1287
1288 SDL_SubmitGPUCommandBuffer(cmd);
1289 SDL_ReleaseGPUTransferBuffer(state->device, tbuf);
1290 return true;
1291}
1292
1293static bool resize(DTTR_BackendState *state, int width, int height) {
1295}
1296
1297// Converts SDL GPU driver identifiers into labels suitable for the window title.
1298static const char *driver_display_name(const char *driver) {
1299 if (strcmp(driver, DTTR_DRIVER_VULKAN) == 0) {
1300 return DRIVER_DISPLAY_VULKAN;
1301 }
1302
1303 if (strcmp(driver, DTTR_DRIVER_DIRECT3D12) == 0) {
1305 }
1306
1307 return driver;
1308}
1309
1310static const char *get_driver_name(const DTTR_BackendState *state) {
1311 return driver_display_name(SDL_GetGPUDeviceDriver(state->device));
1312}
1313
1314static const DTTR_RendererVtbl renderer = {
1315 .begin_frame = begin_frame,
1316 .end_frame = end_frame,
1317 .present_video_frame_bgra = present_video_frame_bgra,
1318 .resize = resize,
1319 .cleanup = cleanup,
1320 .get_driver_name = get_driver_name,
1321 .defer_texture_destroy = defer_texture_destroy,
1322};
static void defer_texture_destroy(DTTR_BackendState *state, int texture_index)
static void begin_frame(DTTR_BackendState *state)
static const char * get_driver_name(const DTTR_BackendState *state)
static void end_frame(DTTR_BackendState *state)
static void cleanup(DTTR_BackendState *state)
static const DTTR_RendererVtbl renderer
static bool present_video_frame_bgra(DTTR_BackendState *state, const uint8_t *pixels, int width, int height, int stride)
#define DRIVER_DISPLAY_VULKAN
static void begin_clear_pass(DTTR_BackendState *state, const DTTR_BatchRecord *rec, graphics_replay_state *replay_state)
static void release_window_device(DTTR_BackendState *state)
static SDL_GPUTransferBuffer * create_upload_buffer(DTTR_BackendState *state, uint32_t bytes)
static void generate_pending_mipmaps(DTTR_BackendState *state, SDL_GPUCommandBuffer *cmd, const graphics_pending_upload *pending, int pending_count, uint32_t *uploaded_texture_count, uint64_t *uploaded_bytes)
static void defer_texture_destroy(DTTR_BackendState *state, int texture_index)
static bool try_create_device_for_driver(DTTR_BackendState *state, const SDL_GPUShaderFormat requested_formats, const char *driver)
static bool ensure_video_texture(DTTR_BackendState *state, int width, int height)
static void set_default_viewport(const DTTR_BackendState *state)
static SDL_GPUSampleCount select_msaa_sample_count(DTTR_BackendState *state)
static void end_render_pass_if_active(DTTR_BackendState *state)
static void release_deferred_texture_destroys(DTTR_BackendState *state)
static void begin_frame(DTTR_BackendState *state)
bool dttr_graphics_sdl3gpu_init(DTTR_BackendState *state)
static const char * get_driver_name(const DTTR_BackendState *state)
static void release_upload_pool_slot(DTTR_BackendState *state, int pool_slot)
static SDL_GPUSampleCount msaa_sample_count_from_config(int value)
static void upload_pending_textures(DTTR_BackendState *state, SDL_GPUCommandBuffer *cmd)
static bool create_device(DTTR_BackendState *state)
static bool msaa_enabled(const DTTR_BackendState *state)
static graphics_replay_stats replay_batch_records(DTTR_BackendState *state)
static bool resize(DTTR_BackendState *state, int width, int height)
static const char * graphics_api_driver_name(DTTR_GraphicsApi api)
static void draw_batch_record(DTTR_BackendState *state, const DTTR_BatchRecord *rec, graphics_replay_state *replay_state, graphics_replay_stats *replay_stats)
static void destroy_device(DTTR_BackendState *state)
static bool begin_draw_pass_if_needed(DTTR_BackendState *state)
static void bind_frame_vertex_buffer(const DTTR_BackendState *state, SDL_GPURenderPass *render_pass)
static bool upload_texture_data(DTTR_BackendState *state, SDL_GPUCopyPass *copy, SDL_GPUTexture *tex, void *pixels, int width, int height, uint32_t bytes)
static void end_frame(DTTR_BackendState *state)
static int msaa_sample_count_to_int(SDL_GPUSampleCount value)
#define DRIVER_DISPLAY_DIRECT3D12
static int acquire_upload_pool_slot(DTTR_BackendState *state, uint32_t bytes)
static void cleanup(DTTR_BackendState *state)
static const char * driver_display_name(const char *driver)
static int collect_and_upload_pending(DTTR_BackendState *state, SDL_GPUCopyPass *copy, graphics_pending_upload *pending_uploads, int max_uploads)
static void reset_replay_state(graphics_replay_state *replay_state)
static bool present_video_frame_bgra(DTTR_BackendState *state, const uint8_t *pixels, int width, int height, int stride)
bool dttr_graphics_sdl3gpu_create_pipelines()
Builds all graphics pipelines used by the SDL3 GPU backend.
bool dttr_graphics_sdl3gpu_resize_render_textures(int width, int height)
Recreates resolution-dependent render textures after updating target size.
bool dttr_graphics_sdl3gpu_create_resources()
Creates shared GPU resources used by the SDL3 GPU backend.
DTTR_Graphics_COM_Direct3DDevice7 DWORD block DTTR_Graphics_COM_Direct3DDevice7 DWORD block DTTR_Graphics_COM_Direct3DDevice7 void void void void DWORD f DTTR_Graphics_COM_Direct3DDevice7 DWORD idx float
DTTR_Graphics_COM_Direct3DDevice7 void *status DTTR_Graphics_COM_Direct3DDevice7 DWORD DWORD void DWORD DWORD f DTTR_Graphics_COM_Direct3DDevice7 DWORD void DWORD st
size_t stride
const DTTR_BackendState * state
DTTR_Graphics_COM_Direct3DDevice7 DWORD block DTTR_Graphics_COM_Direct3DDevice7 DWORD block DTTR_Graphics_COM_Direct3DDevice7 void * dst
const uint8_t * src
DTTR_Graphics_COM_DirectDraw7 *self DWORD DWORD h
void DWORD DWORD * free
DTTR_Graphics_COM_DirectDrawSurface7 DWORD flags void NULL
DTTR_GraphicsApi
Definition dttr_config.h:36
@ DTTR_GRAPHICS_API_DIRECT3D12
Definition dttr_config.h:39
@ DTTR_GRAPHICS_API_VULKAN
Definition dttr_config.h:38
#define DTTR_DRIVER_VULKAN
Definition dttr_config.h:31
@ DTTR_SCALING_METHOD_LOGICAL
Definition dttr_config.h:22
#define DTTR_DRIVER_DIRECT3D12
Definition dttr_config.h:32
@ DTTR_SCALING_MODE_STRETCH
Definition dttr_config.h:16
@ DTTR_SCALING_MODE_INTEGER
Definition dttr_config.h:17
DTTR_Config dttr_config
Definition defaults.c:53
#define DTTR_LOG_WARN(...)
Definition dttr_log.h:30
#define DTTR_LOG_INFO(...)
Definition dttr_log.h:29
#define DTTR_LOG_ERROR(...)
Definition dttr_log.h:31
void dttr_graphics_mod_present_rect_before(DTTR_BackendState *state, const DTTR_PresentRect *present)
Definition graphics.c:424
void dttr_graphics_mod_present_rect_after(DTTR_BackendState *state, const DTTR_PresentRect *present, bool overlay_rendered)
Definition graphics.c:439
#define DTTR_PIPELINE_COUNT
#define DTTR_SAMPLER_COUNT
SDL_GPUShaderFormat dttr_graphics_select_shader_format_for_driver(const char *driver, SDL_GPUShaderFormat formats)
Definition util.c:169
bool dttr_graphics_ensure_staged_texture(DTTR_BackendState *state, DTTR_StagedTexture *st)
Definition util.c:14
#define DTTR_CLEAR_COLOR
bool dttr_graphics_is_gpu_thread()
Definition util.c:182
#define DTTR_UPLOAD_POOL_SIZE
SDL_GPUShaderFormat dttr_graphics_requested_shader_formats()
Definition util.c:136
#define DTTR_VERTEX_SIZE
@ DTTR_BACKEND_SDL_GPU
static void dttr_graphics_mod_before_game_frame(DTTR_BackendState *)
#define DTTR_CLEAR_DEPTH
@ DTTR_BATCH_CLEAR
DTTR_PresentRect dttr_graphics_compute_present_rect(int dst_w, int dst_h, int src_w, int src_h, bool stretch, bool integer_fit, float fallback_scale)
Definition util.c:48
#define DTTR_PIPELINE_INDEX(bmode, dtest, dwrite)
const char * dttr_graphics_shader_format_name(SDL_GPUShaderFormat format)
Definition util.c:121
static void dttr_graphics_mod_after_game_frame(DTTR_BackendState *)
static void dttr_graphics_mod_frame_begin(DTTR_BackendState *)
static void dttr_graphics_mod_frame_end(DTTR_BackendState *)
#define DTTR_MAX_STAGED_TEXTURES
void dttr_imgui_render_game_sdl3gpu(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *render_target, uint32_t w, uint32_t h)
void dttr_imgui_render_sdl3gpu(SDL_GPUCommandBuffer *cmd, SDL_GPUTexture *swapchain_tex, uint32_t swap_w, uint32_t swap_h, uint32_t game_x, uint32_t game_y, uint32_t game_w, uint32_t game_h)
A recorded clear or draw command replayed during frame submission.
SDL_GPUSampler * sampler
struct DTTR_BatchRecord::@173304276063167267021134124022136033175102267147::@317066375077304207167347173122133172104242212006 clear
struct DTTR_BatchRecord::@173304276063167267021134124022136033175102267147::@107356260052026241174121242151011150024304321020 draw
SDL_GPUTexture * texture
DTTR_Uniforms uniforms
DTTR_BatchRecordType type
Game-image placement within the present target, in target pixels.
Backend-specific operations dispatched through function pointers.
One reusable upload slot holding a transfer buffer for texture uploads.
SDL_GPUTransferBuffer * transfer_buffer
SDL_GPUSampler * last_sampler
SDL_GPUTexture * last_texture
SDL3GPU backend-private deferred texture destroy queue.
SDL_GPUTexture * deferred_destroys[DTTR_MAX_STAGED_TEXTURES]