MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
3#include "mxvk/mxvk_cv.hpp"
6#if defined(MXVK_WITH_FFMPEG_CAPTURE)
8#endif
10#if defined(MXWRITE_ENABLED)
11#include "mxwrite.hpp"
12#endif
13#include <algorithm>
14#include <array>
15#include <cmath>
16#include <cstdint>
17#include <cstdlib>
18#include <cstring>
19#include <ctime>
20#include <fstream>
21#include <iostream>
22#include <opencv2/opencv.hpp>
23#include <string>
24#include <string_view>
25#include <thread>
26#include <vector>
27#ifdef MXVK_CUDA
28#include <cuda_runtime_api.h>
29#include <opencv2/core/cuda.hpp>
30#include <opencv2/cudaimgproc.hpp>
31#include <opencv2/cudawarping.hpp>
32#include <unistd.h>
33#endif
34
35#ifndef compute_shader_ASSET_DIR
36#define compute_shader_ASSET_DIR "."
37#endif
38
39static constexpr int HISTORY_SIZE = 8;
40static constexpr const char *MODE_SHADER_NAME = "acidcam_filters.spv";
41static constexpr std::array<std::string_view, 50> ACIDCAM_FILTER_MODE_NAMES = {
42 "Block Pixelate", "Block Mirror X", "Block Mirror Y", "Combine Pixels", "History XOR", "Temporal Blend", "Scanline Warp", "RGB Split", "Horizontal Mirror", "Vertical Mirror", "Kaleidoscope", "Dynamic Kaleidoscope", "Negate", "Posterize", "Threshold", "Gamma Darken", "Brightness Contrast", "Sepia", "Solarize", "Hue Rotate", "Saturate", "Desaturate", "Box Blur", "Sharpen", "Emboss", "Sobel", "Edge Detect", "Dilate", "Erode", "Posterize Scale", "Wave", "Ripple", "Twirl", "Zoom Pulse", "Crosshatch", "Noise Grain", "Strobe Bars", "Scanline XOR", "Block Shuffle", "Diagonal Slice", "Frame Blend", "Trail Blend", "History Median", "Row Blend", "Column Blend", "Color Cycle", "Gradient Ramp", "Flash Invert", "XOR Grid", "Mirror Trail",
43};
44
45struct ComputePC {
46 int32_t mode;
47 int32_t historyCount;
48 int32_t historyIdx;
49 int32_t square_size;
50 int32_t history_dir;
51 float alpha;
52 int32_t do_swap;
53 int32_t do_invert;
54};
55
57 public:
58 explicit ComputeWindow(const Arguments &args) : mxvk::VK_Window("-[ VK Compute CV ]-", args.width, args.height, args.fullscreen, MXVK_VALIDATION, args.enable_vsync), assetRoot(args.path.empty() ? std::string(compute_shader_ASSET_DIR) : args.path), inputFilename(args.filename), usingFile(!inputFilename.empty()), fastMode(args.fast), explicitResolution(args.resolutionSpecified), fullscreenMode(args.fullscreen), outputFilename(args.output), outputCrf(args.crf), encodePreset(args.encodePreset), encodeTune(args.encodeTune), encodeCodec(args.encodeCodec), encodeRealtime(args.encodeRealtime), mxwriteBlockWhenFull(args.mxwriteBlockWhenFull), repeat(args.repeat), cameraIndex(args.camera_index), requestedShaderIndex(args.shader_index), initialShaderMode(args.index > 0 ? std::clamp(args.index - 1, 0, static_cast<int>(ACIDCAM_FILTER_MODE_NAMES.size()) - 1) : 0) {
59 recordWidth = args.width;
60 recordHeight = args.height;
61 shaderMode = initialShaderMode;
62 initComputeResources();
63 }
64
65 ~ComputeWindow() override {
66 capture.close();
67#if defined(MXVK_WITH_FFMPEG_CAPTURE)
68 ffCapture.close();
69#endif
70 destroyComputeResources();
71 }
72
73 void proc() override {
74 bool frameUploaded = false;
75 if (usingFile && !fastMode) {
76 throttleVideoPlayback();
77 }
78
79#if defined(MXVK_WITH_FFMPEG_CAPTURE)
80 if (usingFfCapture) {
81 frameUploaded = readFfFrameToCompute();
82 if (!frameUploaded && usingFile) {
83 if (repeat) {
84 restartFfCapture();
85 frameUploaded = readFfFrameToCompute();
86 } else {
87 std::cout << "compute_shader: video file reached EOF, shutting down\n";
88 exit();
89 return;
90 }
91 }
92 } else
93#endif
94 {
95 cv::Mat frame;
96#ifdef MXVK_CUDA
97 cv::cuda::GpuMat gpuFrame;
98 if (capture.readGpuRgba(gpuFrame) && !gpuFrame.empty()) {
99 frameUploaded = uploadGpuFrameToCompute(gpuFrame, capture.cudaStream());
100 if (frameUploaded && !captureUploadPathLogged) {
101 std::cout << "compute_shader: CUDA interop capture path active: capture -> GpuMat -> optional CUDA resize -> cudaMemcpy2DToArrayAsync -> Vulkan compute storage image (no download)\n";
102 captureUploadPathLogged = true;
103 }
104 }
105#endif
106 if (!frameUploaded && capture.readRgba(frame) && !frame.empty()) {
107 if (!captureUploadPathLogged) {
108#ifdef MXVK_CUDA
109 std::cout << "compute_shader: CUDA interop unavailable; fallback path active: CUDA/CPU RGBA -> optional CPU resize -> Vulkan staging upload\n";
110#else
111 std::cout << "compute_shader: CPU capture path active: readRgba converts to RGBA, optional CPU resize, then uploads through Vulkan staging\n";
112#endif
113 captureUploadPathLogged = true;
114 }
115 uploadCpuFrameToCompute(frame);
116 frameUploaded = true;
117 } else if (!frameUploaded && usingFile) {
118 if (repeat) {
119 capture.close();
120 if (capture.open(inputFilename)) {
121 configureVideoPlaybackRate();
122 cv::Mat restartedFrame;
123 if (capture.readRgba(restartedFrame) && !restartedFrame.empty()) {
124 if (!captureUploadPathLogged) {
125#ifdef MXVK_CUDA
126 std::cout << "compute_shader: CUDA interop unavailable; fallback path active: CUDA/CPU RGBA -> optional CPU resize -> Vulkan staging upload\n";
127#else
128 std::cout << "compute_shader: CPU capture path active: readRgba converts to RGBA, optional CPU resize, then uploads through Vulkan staging\n";
129#endif
130 captureUploadPathLogged = true;
131 }
132 uploadCpuFrameToCompute(restartedFrame);
133 frameUploaded = true;
134 }
135 }
136 } else {
137 std::cout << "compute_shader: video file reached EOF, shutting down\n";
138 exit();
139 return;
140 }
141 }
142 }
143
144 if (frameUploaded) {
145 tickAnimState();
146 runComputeFrame();
147 recordProcessedFrame();
148 }
149 updateFpsOverlay(frameUploaded);
150 }
151
152 void onSwapchainRecreated() override { rebuildDisplayPipeline(); }
153
154 void onRecordCustomRendering(VkCommandBuffer cmd, [[maybe_unused]] uint32_t imageIndex) override { renderComputeOutput(cmd); }
155
156 void event(SDL_Event &e) override {
157 if (e.type == SDL_EVENT_QUIT || (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE)) {
158 exit();
159 return;
160 }
161
162 if (e.type == SDL_EVENT_KEY_DOWN && !spvFiles.empty()) {
163 if (e.key.key == SDLK_UP) {
164 currentSpvIndex = (currentSpvIndex - 1 + static_cast<int>(spvFiles.size())) % static_cast<int>(spvFiles.size());
165 std::cout << "Current index: " << spvFiles[currentSpvIndex] << "\n";
166 reloadPipeline();
167 } else if (e.key.key == SDLK_DOWN) {
168 currentSpvIndex = (currentSpvIndex + 1) % static_cast<int>(spvFiles.size());
169 std::cout << "Current index: " << spvFiles[currentSpvIndex] << "\n";
170 reloadPipeline();
171 } else if (spvFiles[currentSpvIndex] == MODE_SHADER_NAME && (e.key.key == SDLK_LEFT || e.key.key == SDLK_RIGHT)) {
172 const int delta = (e.key.key == SDLK_LEFT) ? -1 : 1;
173 shaderMode = (shaderMode + delta + 50) % 50;
174 std::cout << "Mode shader mode: " << shaderMode << "\n";
175 }
176 }
177 }
178
179 private:
180 struct ComputeImage {
181 VkImage image = VK_NULL_HANDLE;
182 VkDeviceMemory memory = VK_NULL_HANDLE;
183 VkImageView view = VK_NULL_HANDLE;
184#ifdef MXVK_CUDA
185 VkDeviceSize cudaExportMemorySize = 0;
186 cudaExternalMemory_t cudaExternalMemory = nullptr;
187 cudaMipmappedArray_t cudaMipmappedArray = nullptr;
188 cudaArray_t cudaArray = nullptr;
189 bool cudaInteropEnabled = false;
190 bool cudaInteropUnavailableLogged = false;
191 bool cudaUploadLogged = false;
192 bool cudaBarrierLogged = false;
193#endif
194 };
195
196 std::string assetRoot;
197 mxvk::VK_Capture capture{};
198#if defined(MXVK_WITH_FFMPEG_CAPTURE)
199 mxvk::VK_FF_Capture ffCapture{};
200 bool usingFfCapture = false;
201 std::vector<uint8_t> ffFrameRgba{};
202 int ffFramePitch = 0;
203#ifdef MXVK_CUDA
204 cv::cuda::Stream ffCudaStream{};
205#endif
206#endif
207 std::string inputFilename;
208 bool usingFile = false;
209 bool fastMode = false;
210 bool explicitResolution = false;
211 bool fullscreenMode = false;
212 int recordWidth = 1920;
213 int recordHeight = 1080;
214 int sourceWidth = 1920;
215 int sourceHeight = 1080;
216 std::string outputFilename;
217 std::string outputCrf;
218 std::string encodePreset;
219 std::string encodeTune;
220 std::string encodeCodec;
221 bool encodeRealtime = false;
222 bool mxwriteBlockWhenFull = false;
223 bool repeat = false;
224 mxvk::Font fpsFont{};
225 int cameraIndex = 0;
226 int texWidth = 1920;
227 int texHeight = 1080;
228
229 std::array<ComputeImage, 2> workImg{};
230 std::array<ComputeImage, HISTORY_SIZE> histImg{};
231 ComputeImage outImg{};
232
233 VkSampler computeSampler = VK_NULL_HANDLE;
234
235 VkBuffer stagingBuf = VK_NULL_HANDLE;
236 VkDeviceMemory stagingMem = VK_NULL_HANDLE;
237 VkBuffer readbackBuf = VK_NULL_HANDLE;
238 VkDeviceMemory readbackMem = VK_NULL_HANDLE;
239
240 VkDescriptorSetLayout compDSLayout = VK_NULL_HANDLE;
241 VkPipelineLayout compPipeLayout = VK_NULL_HANDLE;
242 VkPipeline compPipeline = VK_NULL_HANDLE;
243 VkDescriptorPool compDSPool = VK_NULL_HANDLE;
244
245 std::array<VkDescriptorSet, 2> blurDS{};
246 std::array<VkDescriptorSet, 2> blendDS{};
247
248 VkDescriptorSetLayout displayDSLayout = VK_NULL_HANDLE;
249 VkDescriptorPool displayDSPool = VK_NULL_HANDLE;
250 VkDescriptorSet displayDS = VK_NULL_HANDLE;
251 VkPipelineLayout displayPipeLayout = VK_NULL_HANDLE;
252 VkPipeline displayPipeline = VK_NULL_HANDLE;
253 VkBuffer displayVertexBuffer = VK_NULL_HANDLE;
254 VkDeviceMemory displayVertexMemory = VK_NULL_HANDLE;
255 VkBuffer displayIndexBuffer = VK_NULL_HANDLE;
256 VkDeviceMemory displayIndexMemory = VK_NULL_HANDLE;
257
258 int historyIndex = 0;
259 int historyCount = 0;
260 int currentSquare = 4;
261 int squareDir = 1;
262 int currentHistIdx = 0;
263 int currentDir = 1;
264 int requestedShaderIndex = 0;
265 int shaderMode = 0;
266 int initialShaderMode = 0;
267 float alpha = 1.0f;
268 bool captureUploadPathLogged = false;
269 double videoFps = 0.0;
270 double sourceFps = 0.0;
271 std::chrono::duration<double> videoFrameInterval{0.0};
272 std::chrono::steady_clock::time_point nextVideoFrameDeadline{std::chrono::steady_clock::now()};
273 double currentFps = 0.0;
274 uint32_t fpsFrameCount = 0;
275 std::chrono::steady_clock::time_point fpsSampleTime{std::chrono::steady_clock::now()};
276 std::chrono::steady_clock::time_point playbackStartTime{std::chrono::steady_clock::now()};
277 std::string fpsText = "FPS: --";
278 uint64_t processedVideoFrames = 0;
279 bool recordingEnabled = false;
280 [[maybe_unused]] bool recordingWarningLogged = false;
281 bool processedRecordPathLogged = false;
282 std::vector<uint8_t> recordScratch{};
283#ifdef MXVK_CUDA
284 cv::cuda::Stream processedRecordStream{};
285 cv::cuda::GpuMat processedRecordGpuFrame{};
286 cv::cuda::GpuMat computeInputGpuFrame{};
287#endif
288#if defined(MXWRITE_ENABLED)
289 Writer videoWriter{};
290 bool videoWriterOpen = false;
291#endif
292
293 std::vector<std::string> spvFiles{};
294 int currentSpvIndex = 0;
295
296 void loadSPV() {
297 std::ifstream file(assetRoot + "/data/index.txt");
298 if (!file.is_open()) {
299 throw mxvk::Exception("Cannot open: " + assetRoot + "/data/index.txt");
300 }
301
302 std::string line;
303 while (std::getline(file, line)) {
304 if (!line.empty()) {
305 spvFiles.push_back(line);
306 }
307 }
308
309 if (spvFiles.empty()) {
310 throw mxvk::Exception("index.txt contains no entries");
311 }
312
313 currentSpvIndex = std::clamp(requestedShaderIndex, 0, static_cast<int>(spvFiles.size()) - 1);
314 }
315
316 [[nodiscard]] double configureCameraFps() {
317 static constexpr std::array<double, 3> fpsChoices = {60.0, 30.0, 24.0};
318
319 for (const double requestedFps : fpsChoices) {
320 capture.set(cv::CAP_PROP_FPS, requestedFps);
321 const double reportedFps = capture.get(cv::CAP_PROP_FPS);
322 if (reportedFps > 0.0 && reportedFps + 0.5 >= requestedFps) {
323 return reportedFps;
324 }
325 }
326
327 capture.set(cv::CAP_PROP_FPS, fpsChoices.back());
328 const double reportedFps = capture.get(cv::CAP_PROP_FPS);
329 return (reportedFps > 0.0) ? reportedFps : fpsChoices.back();
330 }
331
332 void resetVideoPlaybackClock() { nextVideoFrameDeadline = std::chrono::steady_clock::now(); }
333
334 void configureVideoPlaybackRate() {
335 double reportedFps = 0.0;
336#if defined(MXVK_WITH_FFMPEG_CAPTURE)
337 if (usingFfCapture) {
338 reportedFps = ffCapture.fps();
339 } else
340#endif
341 {
342 reportedFps = capture.get(cv::CAP_PROP_FPS);
343 }
344 videoFps = (reportedFps > 0.0) ? reportedFps : 30.0;
345 sourceFps = videoFps;
346 if (videoFps <= 0.0) {
347 videoFps = 30.0;
348 sourceFps = videoFps;
349 }
350 videoFrameInterval = std::chrono::duration<double>(1.0 / videoFps);
351 resetVideoPlaybackClock();
352 std::cout << "compute_shader: video file FPS " << videoFps << " fps, fast=" << (fastMode ? "true" : "false") << "\n";
353 }
354
355 bool openVideoSource() {
356#if defined(MXVK_WITH_FFMPEG_CAPTURE)
357 usingFfCapture = false;
358 if (ffCapture.open(inputFilename)) {
359 usingFfCapture = true;
360 std::cout << "compute_shader: FFmpeg capture path active for file input" << (ffCapture.using_hardware_decode() ? " (CUDA decode)\n" : " (software decode)\n");
361 return true;
362 }
363 std::cout << "compute_shader: FFmpeg capture failed; falling back to VK_Capture/OpenCV file input\n";
364#endif
365 return capture.open(inputFilename);
366 }
367
368 void uploadCpuFrameToCompute(const cv::Mat &frame) {
369 if (frame.empty()) {
370 return;
371 }
372 if (frame.cols == texWidth && frame.rows == texHeight) {
373 uploadToImage(frame.ptr(), static_cast<int>(frame.step), workImg[0]);
374 return;
375 }
376
377 cv::Mat resizedFrame;
378 cv::resize(frame, resizedFrame, cv::Size(texWidth, texHeight), 0.0, 0.0, cv::INTER_LINEAR);
379 uploadToImage(resizedFrame.ptr(), static_cast<int>(resizedFrame.step), workImg[0]);
380 }
381
382#ifdef MXVK_CUDA
383 bool uploadGpuFrameToCompute(const cv::cuda::GpuMat &gpuFrame, cv::cuda::Stream &stream) {
384 if (gpuFrame.empty()) {
385 return false;
386 }
387 if (gpuFrame.cols == texWidth && gpuFrame.rows == texHeight) {
388 return uploadGpuToImage(gpuFrame, stream, workImg[0]);
389 }
390
391 cv::cuda::resize(gpuFrame, computeInputGpuFrame, cv::Size(texWidth, texHeight), 0.0, 0.0, cv::INTER_LINEAR, stream);
392 stream.waitForCompletion();
393 return uploadGpuToImage(computeInputGpuFrame, stream, workImg[0]);
394 }
395#endif
396
397#if defined(MXVK_WITH_FFMPEG_CAPTURE)
398 void restartFfCapture() {
399 ffCapture.close();
400 if (ffCapture.open(inputFilename)) {
401 usingFfCapture = true;
402 configureVideoPlaybackRate();
403 }
404 }
405
406 bool readFfFrameToCompute() {
407#ifdef MXVK_CUDA
408 if (ffCapture.using_hardware_decode()) {
409 cv::cuda::GpuMat gpuFrame;
410 if (ffCapture.readGpuRgba(gpuFrame, ffCudaStream) && !gpuFrame.empty()) {
411 const bool uploadedWithInterop = uploadGpuFrameToCompute(gpuFrame, ffCudaStream);
412 if (uploadedWithInterop) {
413 if (!captureUploadPathLogged) {
414 std::cout << "compute_shader: FFmpeg CUDA path active: NVDEC/CUDA decode -> CUDA NV12/RGBA conversion -> optional CUDA resize -> cudaMemcpy2DToArrayAsync -> Vulkan compute storage image (no CPU download)\n";
415 captureUploadPathLogged = true;
416 }
417 return true;
418 }
419
420 cv::Mat cpuFrame;
421 gpuFrame.download(cpuFrame, ffCudaStream);
422 ffCudaStream.waitForCompletion();
423 if (!cpuFrame.empty()) {
424 if (!captureUploadPathLogged) {
425 std::cout << "compute_shader: FFmpeg CUDA decode active, Vulkan CUDA interop unavailable; downloading RGBA for optional CPU resize and staging upload\n";
426 captureUploadPathLogged = true;
427 }
428 uploadCpuFrameToCompute(cpuFrame);
429 return true;
430 }
431 }
432 }
433#endif
434 int frameWidth = 0;
435 int frameHeight = 0;
436 if (!ffCapture.readRgba(ffFrameRgba, frameWidth, frameHeight, ffFramePitch) || ffFrameRgba.empty()) {
437 return false;
438 }
439 if (frameWidth <= 0 || frameHeight <= 0) {
440 return false;
441 }
442 if (!captureUploadPathLogged) {
443 std::cout << "compute_shader: FFmpeg capture path active: decoded RGBA -> optional CPU resize -> Vulkan staging upload\n";
444 captureUploadPathLogged = true;
445 }
446 cv::Mat frame(frameHeight, frameWidth, CV_8UC4, ffFrameRgba.data(), static_cast<size_t>(ffFramePitch));
447 uploadCpuFrameToCompute(frame);
448 return true;
449 }
450#endif
451
452 void configureRecordingDefaults() {
453 if (outputFilename.empty()) {
454 outputFilename = assetRoot + "/compute_shader_output.mp4";
455 }
456 if (outputCrf.empty()) {
457 outputCrf = "24";
458 }
459 }
460
461 [[nodiscard]] int overlayFontSizeForCanvas() const {
462 const int canvasMinDim = std::min(texWidth, texHeight);
463 return std::clamp(canvasMinDim / 30, 8, 36);
464 }
465
466 void maybeResizeWindowToSource() {
467 if (usingFile && !explicitResolution && !fullscreenMode && getSDLWindow() != nullptr) {
468 SDL_SetWindowSize(getSDLWindow(), texWidth, texHeight);
469 std::cout << "compute_shader: window resized to source frame size " << texWidth << "x" << texHeight << " (pass -r/--resolution to override)\n";
470 }
471 }
472
473#if defined(MXWRITE_ENABLED)
474 [[nodiscard]] int parseCrf() const {
475 try {
476 size_t parsedChars = 0;
477 const int value = std::stoi(outputCrf, &parsedChars);
478 if (parsedChars == outputCrf.size() && value >= 0 && value <= 51) {
479 return value;
480 }
481 } catch (const std::exception &) {
482 }
483 throw mxvk::Exception("compute_shader: invalid CRF '" + outputCrf + "'; expected integer 0..51");
484 }
485
486 void openVideoWriter() {
487 if (videoWriterOpen) {
488 return;
489 }
490 if (sourceFps <= 0.0) {
491 sourceFps = usingFile ? videoFps : 30.0;
492 }
493 if (sourceFps <= 0.0) {
494 sourceFps = 30.0;
495 }
496 EncodeOptions encodeOptions{};
497 encodeOptions.crf = parseCrf();
498 if (!encodePreset.empty()) {
499 encodeOptions.preset = encodePreset;
500 }
501 if (!encodeTune.empty()) {
502 encodeOptions.tune = encodeTune;
503 }
504 if (!encodeCodec.empty()) {
505 encodeOptions.codec = encodeCodec;
506 }
507 encodeOptions.realtime = encodeRealtime;
508 encodeOptions.block_when_full = mxwriteBlockWhenFull;
509
510 if (!videoWriter.open(outputFilename, recordWidth, recordHeight, static_cast<float>(sourceFps), encodeOptions)) {
511 throw mxvk::Exception("compute_shader: failed to open MXWrite output file '" + outputFilename + "'");
512 }
513 videoWriterOpen = true;
514 std::cout << "compute_shader: recording to " << outputFilename << " at " << sourceFps << " fps with crf " << encodeOptions.crf << ", codec=" << encodeOptions.codec << ", preset=" << encodeOptions.preset << ", tune=" << (encodeOptions.tune.empty() ? "none" : encodeOptions.tune) << ", realtime=" << (encodeOptions.realtime ? "true" : "false") << ", block_when_full=" << (mxwriteBlockWhenFull ? "true" : "false") << "\n";
515 }
516
517 void recordFrame(const cv::Mat &frame) {
518 if (videoWriterOpen && !frame.empty()) {
519 recordFrame(frame.ptr(), frame.cols, frame.rows, static_cast<int>(frame.step));
520 }
521 }
522
523 void recordFrame(const uint8_t *data, int width, int height, int pitch) {
524 if (!videoWriterOpen || data == nullptr || width != recordWidth || height != recordHeight) {
525 return;
526 }
527
528 const int tightPitch = recordWidth * 4;
529 if (pitch == tightPitch) {
530 videoWriter.write(const_cast<uint8_t *>(data));
531 return;
532 }
533
534 if (pitch < tightPitch) {
535 return;
536 }
537
538 const int recordTightPitch = recordWidth * 4;
539 recordScratch.resize(static_cast<size_t>(recordTightPitch) * static_cast<size_t>(recordHeight));
540 for (int row = 0; row < recordHeight; ++row) {
541 std::memcpy(recordScratch.data() + static_cast<size_t>(row) * static_cast<size_t>(tightPitch), data + static_cast<size_t>(row) * static_cast<size_t>(pitch), static_cast<size_t>(recordTightPitch));
542 }
543 videoWriter.write(recordScratch.data());
544 }
545
546#ifdef MXVK_CUDA
547 void recordGpuFrame(cv::cuda::GpuMat &gpuFrame) {
548 if (!videoWriterOpen || gpuFrame.empty()) {
549 return;
550 }
551#if defined(MXWRITE_HAS_CUDA_COPY)
552 if (videoWriter.is_hardware_encode() && videoWriter.write_cuda_rgba(gpuFrame.ptr(), static_cast<int>(gpuFrame.step))) {
553 return;
554 }
555#endif
556 cv::Mat cpuFrame;
557 gpuFrame.download(cpuFrame);
558 recordFrame(cpuFrame);
559 }
560#endif
561#else
562 void openVideoWriter() {
563 if (!recordingEnabled) {
564 return;
565 }
566 if (!recordingWarningLogged) {
567 std::cout << "compute_shader: MXWrite is unavailable; video recording disabled\n";
568 recordingWarningLogged = true;
569 }
570 recordingEnabled = false;
571 }
572
573 void recordFrame(const cv::Mat &) {}
574 void recordFrame(const uint8_t *, int, int, int) {}
575#ifdef MXVK_CUDA
576 void recordGpuFrame(cv::cuda::GpuMat &) {}
577#endif
578#endif
579
580#ifdef MXVK_CUDA
581 bool recordProcessedFrameCuda() {
582 if (!recordingEnabled || !ensureCudaInterop(outImg)) {
583 return false;
584 }
585
586 const VkCommandBuffer cmd = beginSingleTimeCommands();
587 transitionImageLayout(cmd, outImg.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT);
588 endSingleTimeCommands(cmd);
589
590 processedRecordGpuFrame.create(texHeight, texWidth, CV_8UC4);
591 cudaStream_t cudaStream = mxvk::cuda_stream_handle(processedRecordStream);
592 cudaError_t cudaResult = cudaMemcpy2DFromArrayAsync(processedRecordGpuFrame.ptr(), processedRecordGpuFrame.step, outImg.cudaArray, 0, 0, static_cast<size_t>(texWidth) * 4U, static_cast<size_t>(texHeight), cudaMemcpyDeviceToDevice, cudaStream);
593 if (cudaResult != cudaSuccess) {
594 std::cout << "compute_shader: CUDA processed-frame readback failed: " << cudaGetErrorString(cudaResult) << "\n";
595 return false;
596 }
597
598 cudaResult = cudaStreamSynchronize(cudaStream);
599 if (cudaResult != cudaSuccess) {
600 std::cout << "compute_shader: CUDA processed-frame readback sync failed: " << cudaGetErrorString(cudaResult) << "\n";
601 return false;
602 }
603
604 if (!processedRecordPathLogged) {
605 std::cout << "compute_shader: processed recording path active: Vulkan compute output image -> CUDA array -> RGBA GpuMat -> MXWrite"
606#if defined(MXWRITE_HAS_CUDA_COPY)
607 << " CUDA ingestion when hardware encode is active"
608#else
609 << " CPU fallback when MXWrite CUDA ingestion is unavailable"
610#endif
611 << "\n";
612 processedRecordPathLogged = true;
613 }
614 recordGpuFrame(processedRecordGpuFrame);
615 return true;
616 }
617#endif
618
619 void recordProcessedFrame() {
620 if (!recordingEnabled || readbackBuf == VK_NULL_HANDLE || readbackMem == VK_NULL_HANDLE) {
621 return;
622 }
623
624#ifdef MXVK_CUDA
625 if (recordProcessedFrameCuda()) {
626 return;
627 }
628#endif
629
630 const int tightPitch = texWidth * 4;
631 const VkDeviceSize bytes = static_cast<VkDeviceSize>(tightPitch) * static_cast<VkDeviceSize>(texHeight);
632 const VkCommandBuffer cmd = beginSingleTimeCommands();
633
634 transitionImageLayout(cmd, outImg.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
635
636 VkBufferImageCopy2 region{};
637 region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2;
638 region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
639 region.imageExtent = {static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), 1};
640
641 VkCopyImageToBufferInfo2 copyInfo{};
642 copyInfo.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2;
643 copyInfo.srcImage = outImg.image;
644 copyInfo.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
645 copyInfo.dstBuffer = readbackBuf;
646 copyInfo.regionCount = 1;
647 copyInfo.pRegions = &region;
648 vkCmdCopyImageToBuffer2(cmd, &copyInfo);
649
650 transitionImageLayout(cmd, outImg.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
651
652 endSingleTimeCommands(cmd);
653
654 void *mapped = nullptr;
655 VK_CHECK_RESULT(vkMapMemory(device, readbackMem, 0, bytes, 0, &mapped));
656 cv::Mat sourceFrame(texHeight, texWidth, CV_8UC4, mapped, tightPitch);
657 if (!processedRecordPathLogged) {
658 std::cout << "compute_shader: processed recording path active: Vulkan compute output image -> readback buffer -> MXWrite\n";
659 processedRecordPathLogged = true;
660 }
661 recordFrame(sourceFrame);
662 vkUnmapMemory(device, readbackMem);
663 }
664
665 void throttleVideoPlayback() {
666 if (!usingFile || fastMode || videoFps <= 0.0) {
667 return;
668 }
669
670 const auto now = std::chrono::steady_clock::now();
671 if (nextVideoFrameDeadline > now) {
672 std::this_thread::sleep_until(nextVideoFrameDeadline);
673 }
674 nextVideoFrameDeadline += std::chrono::duration_cast<std::chrono::steady_clock::duration>(videoFrameInterval);
675 }
676
677 void initComputeResources() {
678 try {
679 if (device == VK_NULL_HANDLE) {
680 throw mxvk::Exception("Compute resources require an initialized Vulkan device");
681 }
682 if (swapchain == VK_NULL_HANDLE || command_pool == VK_NULL_HANDLE) {
683 createDevice();
684 }
685 setFont(assetRoot + "/data/font.ttf", 20);
686
687 if (usingFile) {
688 if (!openVideoSource()) {
689 throw mxvk::Exception("Failed to open video file " + inputFilename);
690 }
691 configureVideoPlaybackRate();
692 } else if (!capture.open(cameraIndex)) {
693 throw mxvk::Exception("Failed to open camera " + std::to_string(cameraIndex));
694 }
695
696 if (!usingFile) {
697 capture.set(cv::CAP_PROP_FRAME_WIDTH, recordWidth);
698 capture.set(cv::CAP_PROP_FRAME_HEIGHT, recordHeight);
699 const double selectedFps = configureCameraFps();
700 sourceFps = selectedFps;
701 std::cout << "compute_shader: requested camera FPS fallback order 60 -> 30 -> 24; selected " << selectedFps << " fps\n";
702 }
703
704#if defined(MXVK_WITH_FFMPEG_CAPTURE)
705 if (usingFfCapture) {
706 sourceWidth = ffCapture.width();
707 sourceHeight = ffCapture.height();
708 if (sourceWidth <= 0 || sourceHeight <= 0) {
709 throw mxvk::Exception("Failed to query FFmpeg video dimensions");
710 }
711 } else
712#endif
713 {
714 cv::Mat frame;
715 if (!capture.read(frame) || frame.empty()) {
716 throw mxvk::Exception(usingFile ? "Failed to read initial video frame" : "Failed to read initial camera frame");
717 }
718
719 sourceWidth = frame.cols;
720 sourceHeight = frame.rows;
721 }
722
723 if (explicitResolution) {
724 texWidth = recordWidth;
725 texHeight = recordHeight;
726 std::cout << "compute_shader: compute canvas set from explicit resolution " << texWidth << "x" << texHeight << "; source frames are " << sourceWidth << "x" << sourceHeight << "\n";
727 } else {
728 texWidth = sourceWidth;
729 texHeight = sourceHeight;
730 recordWidth = texWidth;
731 recordHeight = texHeight;
732 }
733
734 configureRecordingDefaults();
735 recordingEnabled = true;
736 openVideoWriter();
737 fpsFont.reset(assetRoot + "/data/font.ttf", overlayFontSizeForCanvas());
738 playbackStartTime = std::chrono::steady_clock::now();
739 maybeResizeWindowToSource();
740
741 const VkDeviceSize imgBytes = static_cast<VkDeviceSize>(texWidth) * texHeight * 4;
742
743 createBuffer(imgBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuf, stagingMem);
744 createBuffer(imgBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, readbackBuf, readbackMem);
745
746 {
747 const VkCommandBuffer cmd = beginSingleTimeCommands();
748#ifdef MXVK_CUDA
749 allocCImg(workImg[0], cmd, true);
750#else
751 allocCImg(workImg[0], cmd);
752#endif
753 allocCImg(workImg[1], cmd);
754 for (ComputeImage &img : histImg) {
755 allocCImg(img, cmd);
756 }
757#ifdef MXVK_CUDA
758 allocCImg(outImg, cmd, true);
759#else
760 allocCImg(outImg, cmd);
761#endif
762 endSingleTimeCommands(cmd);
763 }
764
765 VkSamplerCreateInfo samplerInfo{};
766 samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
767 samplerInfo.magFilter = VK_FILTER_LINEAR;
768 samplerInfo.minFilter = VK_FILTER_LINEAR;
769 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
770 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
771 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
772 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
773 samplerInfo.maxAnisotropy = 1.0f;
774 VK_CHECK_RESULT(vkCreateSampler(device, &samplerInfo, nullptr, &computeSampler));
775
776 loadSPV();
777 buildDescriptorSetLayout();
778 buildComputePipeline();
779 buildDescriptorSets();
780 createDisplayResources();
781 } catch (...) {
782 // Constructor failure bypasses ~ComputeWindow; cleanup partial Vulkan state here.
783 capture.close();
784#if defined(MXVK_WITH_FFMPEG_CAPTURE)
785 ffCapture.close();
786#endif
787 destroyComputeResources();
788 throw;
789 }
790 }
791
792 void updateFpsOverlay(bool frameUploaded) {
793 if (!active || !frameUploaded) {
794 return;
795 }
796
797 try {
799 } catch (const std::exception &ex) {
800 std::cerr << "compute_shader: failed to clear stale text overlay queue: " << ex.what() << "\n";
801 }
802
803 if (frameUploaded) {
804 ++fpsFrameCount;
805 ++processedVideoFrames;
806 }
807
808 const auto now = std::chrono::steady_clock::now();
809 const double elapsed = std::chrono::duration<double>(now - fpsSampleTime).count();
810 if (elapsed >= 0.25) {
811 currentFps = static_cast<double>(fpsFrameCount) / elapsed;
812 fpsFrameCount = 0;
813 fpsSampleTime = now;
814 fpsText = std::format("FPS: {:.1f}", currentFps);
815 }
816
817 const auto recElapsed = now - playbackStartTime;
818 const uint64_t recTotalSeconds = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(recElapsed).count());
819 const uint64_t recHours = recTotalSeconds / 3600U;
820 const uint64_t recMinutes = (recTotalSeconds / 60U) % 60U;
821 const uint64_t recSeconds = recTotalSeconds % 60U;
822 const std::string recText = std::format("Rec: {:02}:{:02}:{:02}", recHours, recMinutes, recSeconds);
823
824 const double effectiveSourceFps = (sourceFps > 0.0) ? sourceFps : videoFps;
825 const uint64_t totalTenths = (effectiveSourceFps > 0.0) ? static_cast<uint64_t>(std::llround((static_cast<double>(processedVideoFrames) / effectiveSourceFps) * 10.0)) : 0U;
826 const uint64_t hours = totalTenths / 36000U;
827 const uint64_t minutes = (totalTenths / 600U) % 60U;
828 const uint64_t seconds = (totalTenths / 10U) % 60U;
829 const uint64_t tenths = totalTenths % 10U;
830 const std::string timeText = std::format("Output: {:02}:{:02}:{:02}.{:01}", hours, minutes, seconds, tenths);
831 const std::string overlayText = std::format("Source FPS: {:.1f} Current FPS: {:.1f} | {} | {}", sourceFps, currentFps, recText, timeText);
832
833 printText(overlayText, 18, 18, SDL_Color{255, 240, 0, 255}, fpsFont);
834 if (!spvFiles.empty()) {
835 if (spvFiles[currentSpvIndex] == MODE_SHADER_NAME) {
836 const int modeIndex = std::clamp(shaderMode, 0, static_cast<int>(ACIDCAM_FILTER_MODE_NAMES.size()) - 1);
837 const std::string modeText = std::format("Mode: {} {}/{}", ACIDCAM_FILTER_MODE_NAMES[modeIndex], modeIndex + 1, ACIDCAM_FILTER_MODE_NAMES.size());
838 printText(modeText, 15, 68, SDL_Color{255, 105, 180, 255});
839 } else {
840 const std::string spvText = std::format("{}: {}", currentSpvIndex, spvFiles[currentSpvIndex]);
841 printText(spvText, 15, 68, SDL_Color{80, 160, 255, 255});
842 }
843 }
844 }
845
846 [[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
847 VkPhysicalDeviceMemoryProperties memProperties{};
848 vkGetPhysicalDeviceMemoryProperties(physical_device, &memProperties);
849
850 for (uint32_t index = 0; index < memProperties.memoryTypeCount; ++index) {
851 const bool typeMatches = (typeFilter & (1U << index)) != 0U;
852 const bool propertyMatches = (memProperties.memoryTypes[index].propertyFlags & properties) == properties;
853 if (typeMatches && propertyMatches) {
854 return index;
855 }
856 }
857
858 throw mxvk::Exception("Failed to find suitable memory type");
859 }
860
861 void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
862 VkBuffer newBuffer = VK_NULL_HANDLE;
863 VkDeviceMemory newMemory = VK_NULL_HANDLE;
864
865 VkBufferCreateInfo bufferInfo{};
866 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
867 bufferInfo.size = size;
868 bufferInfo.usage = usage;
869 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
870
871 try {
872 VK_CHECK_RESULT(vkCreateBuffer(device, &bufferInfo, nullptr, &newBuffer));
873
874 VkMemoryRequirements memRequirements{};
875 vkGetBufferMemoryRequirements(device, newBuffer, &memRequirements);
876
877 VkMemoryAllocateInfo allocInfo{};
878 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
879 allocInfo.allocationSize = memRequirements.size;
880 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);
881 VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &newMemory));
882 VK_CHECK_RESULT(vkBindBufferMemory(device, newBuffer, newMemory, 0));
883 } catch (...) {
884 if (newBuffer != VK_NULL_HANDLE) {
885 vkDestroyBuffer(device, newBuffer, nullptr);
886 }
887 if (newMemory != VK_NULL_HANDLE) {
888 vkFreeMemory(device, newMemory, nullptr);
889 }
890 throw;
891 }
892
893 if (buffer != VK_NULL_HANDLE) {
894 vkDestroyBuffer(device, buffer, nullptr);
895 }
896 if (bufferMemory != VK_NULL_HANDLE) {
897 vkFreeMemory(device, bufferMemory, nullptr);
898 }
899 buffer = newBuffer;
900 bufferMemory = newMemory;
901 }
902
903 [[nodiscard]] VkCommandBuffer beginSingleTimeCommands() const {
904 VkCommandBufferAllocateInfo allocInfo{};
905 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
906 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
907 allocInfo.commandPool = command_pool;
908 allocInfo.commandBufferCount = 1;
909
910 VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
911 VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer));
912
913 VkCommandBufferBeginInfo beginInfo{};
914 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
915 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
916 VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffer, &beginInfo));
917
918 return commandBuffer;
919 }
920
921 void endSingleTimeCommands(VkCommandBuffer commandBuffer) const {
922 VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer));
923
924 VkCommandBufferSubmitInfo commandBufferInfo{};
925 commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
926 commandBufferInfo.commandBuffer = commandBuffer;
927
928 VkSubmitInfo2 submitInfo{};
929 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
930 submitInfo.commandBufferInfoCount = 1;
931 submitInfo.pCommandBufferInfos = &commandBufferInfo;
932
933 VK_CHECK_RESULT(vkQueueSubmit2(graphics_queue, 1, &submitInfo, VK_NULL_HANDLE));
934 VK_CHECK_RESULT(vkQueueWaitIdle(graphics_queue));
935 vkFreeCommandBuffers(device, command_pool, 1, &commandBuffer);
936 }
937
938 void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage &image, VkDeviceMemory &imageMemory) {
939 VkImage newImage = VK_NULL_HANDLE;
940 VkDeviceMemory newMemory = VK_NULL_HANDLE;
941
942 VkImageCreateInfo imageInfo{};
943 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
944 imageInfo.imageType = VK_IMAGE_TYPE_2D;
945 imageInfo.extent.width = width;
946 imageInfo.extent.height = height;
947 imageInfo.extent.depth = 1;
948 imageInfo.mipLevels = 1;
949 imageInfo.arrayLayers = 1;
950 imageInfo.format = format;
951 imageInfo.tiling = tiling;
952 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
953 imageInfo.usage = usage;
954 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
955 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
956
957 try {
958 VK_CHECK_RESULT(vkCreateImage(device, &imageInfo, nullptr, &newImage));
959
960 VkMemoryRequirements memRequirements{};
961 vkGetImageMemoryRequirements(device, newImage, &memRequirements);
962
963 VkMemoryAllocateInfo allocInfo{};
964 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
965 allocInfo.allocationSize = memRequirements.size;
966 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);
967 VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &newMemory));
968 VK_CHECK_RESULT(vkBindImageMemory(device, newImage, newMemory, 0));
969 } catch (...) {
970 if (newImage != VK_NULL_HANDLE) {
971 vkDestroyImage(device, newImage, nullptr);
972 }
973 if (newMemory != VK_NULL_HANDLE) {
974 vkFreeMemory(device, newMemory, nullptr);
975 }
976 throw;
977 }
978
979 if (image != VK_NULL_HANDLE) {
980 vkDestroyImage(device, image, nullptr);
981 }
982 if (imageMemory != VK_NULL_HANDLE) {
983 vkFreeMemory(device, imageMemory, nullptr);
984 }
985 image = newImage;
986 imageMemory = newMemory;
987 }
988
989#ifdef MXVK_CUDA
990 void createCudaExportableImage(ComputeImage &img) {
991 std::cout << "compute_shader: CUDA interop init: requesting exportable compute input image " << texWidth << "x" << texHeight << " RGBA8 optimal-tiled OPAQUE_FD\n";
992
993 VkExternalMemoryImageCreateInfo externalImageInfo{};
994 externalImageInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO;
995 externalImageInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
996
997 VkImageCreateInfo imageInfo{};
998 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
999 imageInfo.pNext = &externalImageInfo;
1000 imageInfo.imageType = VK_IMAGE_TYPE_2D;
1001 imageInfo.extent.width = static_cast<uint32_t>(texWidth);
1002 imageInfo.extent.height = static_cast<uint32_t>(texHeight);
1003 imageInfo.extent.depth = 1;
1004 imageInfo.mipLevels = 1;
1005 imageInfo.arrayLayers = 1;
1006 imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
1007 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1008 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1009 imageInfo.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
1010 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1011 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1012
1013 VK_CHECK_RESULT(vkCreateImage(device, &imageInfo, nullptr, &img.image));
1014
1015 VkMemoryRequirements memRequirements{};
1016 vkGetImageMemoryRequirements(device, img.image, &memRequirements);
1017
1018 VkExportMemoryAllocateInfo exportMemoryInfo{};
1019 exportMemoryInfo.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO;
1020 exportMemoryInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
1021
1022 VkMemoryAllocateInfo allocInfo{};
1023 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1024 allocInfo.pNext = &exportMemoryInfo;
1025 allocInfo.allocationSize = memRequirements.size;
1026
1027 try {
1028 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1029 VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &img.memory));
1030 VK_CHECK_RESULT(vkBindImageMemory(device, img.image, img.memory, 0));
1031 img.cudaExportMemorySize = memRequirements.size;
1032 img.cudaInteropUnavailableLogged = false;
1033 std::cout << "compute_shader: CUDA interop init: exportable compute input image allocated (memorySize=" << static_cast<unsigned long long>(memRequirements.size) << " bytes, memoryType=" << allocInfo.memoryTypeIndex << "); optimal image memory will be imported as cudaArray\n";
1034 } catch (...) {
1035 if (img.image != VK_NULL_HANDLE) {
1036 vkDestroyImage(device, img.image, nullptr);
1037 img.image = VK_NULL_HANDLE;
1038 }
1039 if (img.memory != VK_NULL_HANDLE) {
1040 vkFreeMemory(device, img.memory, nullptr);
1041 img.memory = VK_NULL_HANDLE;
1042 }
1043 img.cudaExportMemorySize = 0;
1044 throw;
1045 }
1046 }
1047
1048 void destroyCudaInterop(ComputeImage &img) {
1049 if (img.cudaInteropEnabled || img.cudaExternalMemory != nullptr || img.cudaMipmappedArray != nullptr) {
1050 std::cout << "compute_shader: CUDA interop: destroying imported compute input image resources\n";
1051 }
1052 if (img.cudaMipmappedArray != nullptr) {
1053 cudaFreeMipmappedArray(img.cudaMipmappedArray);
1054 img.cudaMipmappedArray = nullptr;
1055 img.cudaArray = nullptr;
1056 }
1057 if (img.cudaExternalMemory != nullptr) {
1058 cudaDestroyExternalMemory(img.cudaExternalMemory);
1059 img.cudaExternalMemory = nullptr;
1060 }
1061 img.cudaInteropEnabled = false;
1062 img.cudaExportMemorySize = 0;
1063 img.cudaUploadLogged = false;
1064 img.cudaBarrierLogged = false;
1065 }
1066
1067 bool ensureCudaInterop(ComputeImage &img) {
1068 if (img.cudaInteropEnabled) {
1069 return true;
1070 }
1071 if (img.memory == VK_NULL_HANDLE || img.cudaExportMemorySize == 0) {
1072 if (!img.cudaInteropUnavailableLogged) {
1073 std::cout << "compute_shader: CUDA interop init: compute input image is not exportable\n";
1074 img.cudaInteropUnavailableLogged = true;
1075 }
1076 return false;
1077 }
1078 if (vkGetMemoryFdKHR == nullptr) {
1079 if (!img.cudaInteropUnavailableLogged) {
1080 std::cout << "compute_shader: CUDA interop init: vkGetMemoryFdKHR was not loaded\n";
1081 img.cudaInteropUnavailableLogged = true;
1082 }
1083 return false;
1084 }
1085
1086 VkMemoryGetFdInfoKHR fdInfo{};
1087 fdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR;
1088 fdInfo.memory = img.memory;
1089 fdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT;
1090
1091 int memoryFd = -1;
1092 const VkResult fdResult = vkGetMemoryFdKHR(device, &fdInfo, &memoryFd);
1093 if (fdResult != VK_SUCCESS) {
1094 if (!img.cudaInteropUnavailableLogged) {
1095 std::cout << "compute_shader: CUDA interop init: vkGetMemoryFdKHR failed (" << static_cast<int>(fdResult) << ")\n";
1096 img.cudaInteropUnavailableLogged = true;
1097 }
1098 return false;
1099 }
1100 std::cout << "compute_shader: CUDA interop init: exported compute input image memory fd=" << memoryFd << "\n";
1101
1102 cudaExternalMemoryHandleDesc externalMemoryDesc{};
1103 externalMemoryDesc.type = cudaExternalMemoryHandleTypeOpaqueFd;
1104 externalMemoryDesc.handle.fd = memoryFd;
1105 externalMemoryDesc.size = img.cudaExportMemorySize;
1106
1107 cudaError_t cudaResult = cudaImportExternalMemory(&img.cudaExternalMemory, &externalMemoryDesc);
1108 if (cudaResult != cudaSuccess) {
1109 close(memoryFd);
1110 if (!img.cudaInteropUnavailableLogged) {
1111 std::cout << "compute_shader: CUDA interop init: cudaImportExternalMemory failed: " << cudaGetErrorString(cudaResult) << "\n";
1112 img.cudaInteropUnavailableLogged = true;
1113 }
1114 img.cudaExternalMemory = nullptr;
1115 return false;
1116 }
1117 std::cout << "compute_shader: CUDA interop init: imported compute input image external memory into CUDA (" << static_cast<unsigned long long>(img.cudaExportMemorySize) << " bytes)\n";
1118
1119 cudaExternalMemoryMipmappedArrayDesc arrayDesc{};
1120 arrayDesc.offset = 0;
1121 arrayDesc.formatDesc = cudaCreateChannelDesc<uchar4>();
1122 arrayDesc.extent = make_cudaExtent(static_cast<size_t>(texWidth), static_cast<size_t>(texHeight), 0);
1123 arrayDesc.flags = cudaArrayColorAttachment;
1124 arrayDesc.numLevels = 1;
1125
1126 cudaResult = cudaExternalMemoryGetMappedMipmappedArray(&img.cudaMipmappedArray, img.cudaExternalMemory, &arrayDesc);
1127 if (cudaResult != cudaSuccess) {
1128 if (!img.cudaInteropUnavailableLogged) {
1129 std::cout << "compute_shader: CUDA interop init: cudaExternalMemoryGetMappedMipmappedArray failed: " << cudaGetErrorString(cudaResult) << "\n";
1130 img.cudaInteropUnavailableLogged = true;
1131 }
1132 destroyCudaInterop(img);
1133 return false;
1134 }
1135 std::cout << "compute_shader: CUDA interop init: mapped compute input CUDA mipmapped array " << texWidth << "x" << texHeight << " uchar4\n";
1136
1137 cudaResult = cudaGetMipmappedArrayLevel(&img.cudaArray, img.cudaMipmappedArray, 0);
1138 if (cudaResult != cudaSuccess) {
1139 if (!img.cudaInteropUnavailableLogged) {
1140 std::cout << "compute_shader: CUDA interop init: cudaGetMipmappedArrayLevel failed: " << cudaGetErrorString(cudaResult) << "\n";
1141 img.cudaInteropUnavailableLogged = true;
1142 }
1143 destroyCudaInterop(img);
1144 return false;
1145 }
1146
1147 img.cudaInteropEnabled = true;
1148 std::cout << "compute_shader: CUDA interop init: direct CUDA-to-compute-input upload is ready\n";
1149 return true;
1150 }
1151#endif
1152
1153 [[nodiscard]] VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) const {
1154 VkImageViewCreateInfo viewInfo{};
1155 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1156 viewInfo.image = image;
1157 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1158 viewInfo.format = format;
1159 viewInfo.subresourceRange.aspectMask = aspectFlags;
1160 viewInfo.subresourceRange.baseMipLevel = 0;
1161 viewInfo.subresourceRange.levelCount = 1;
1162 viewInfo.subresourceRange.baseArrayLayer = 0;
1163 viewInfo.subresourceRange.layerCount = 1;
1164
1165 VkImageView imageView = VK_NULL_HANDLE;
1166 VK_CHECK_RESULT(vkCreateImageView(device, &viewInfo, nullptr, &imageView));
1167 return imageView;
1168 }
1169
1170 void allocCImg(ComputeImage &img, VkCommandBuffer cmd, [[maybe_unused]] bool cudaExportable = false) {
1171#ifdef MXVK_CUDA
1172 if (cudaExportable) {
1173 try {
1174 createCudaExportableImage(img);
1175 } catch (const std::exception &ex) {
1176 std::cout << "compute_shader: CUDA exportable input image unavailable: " << ex.what() << "; using Vulkan staging fallback\n";
1177 createImage(static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, img.image, img.memory);
1178 }
1179 } else {
1180 createImage(static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, img.image, img.memory);
1181 }
1182#else
1183 createImage(static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, img.image, img.memory);
1184#endif
1185 img.view = createImageView(img.image, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
1186
1187 transitionImageLayout(cmd, img.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT);
1188 }
1189
1190 void reloadPipeline() {
1191 vkDeviceWaitIdle(device);
1192 if (compPipeline != VK_NULL_HANDLE) {
1193 vkDestroyPipeline(device, compPipeline, nullptr);
1194 compPipeline = VK_NULL_HANDLE;
1195 }
1196 if (compPipeLayout != VK_NULL_HANDLE) {
1197 vkDestroyPipelineLayout(device, compPipeLayout, nullptr);
1198 compPipeLayout = VK_NULL_HANDLE;
1199 }
1200 shaderMode = initialShaderMode;
1201 buildComputePipeline();
1202 }
1203
1204 void buildDescriptorSetLayout() {
1205 std::array<VkDescriptorSetLayoutBinding, 3> bindings{};
1206
1207 bindings[0].binding = 0;
1208 bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
1209 bindings[0].descriptorCount = 1;
1210 bindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
1211
1212 bindings[1].binding = 1;
1213 bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1214 bindings[1].descriptorCount = 1;
1215 bindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
1216
1217 bindings[2].binding = 2;
1218 bindings[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1219 bindings[2].descriptorCount = HISTORY_SIZE;
1220 bindings[2].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
1221
1222 VkDescriptorSetLayoutCreateInfo createInfo{};
1223 createInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
1224 createInfo.bindingCount = static_cast<uint32_t>(bindings.size());
1225 createInfo.pBindings = bindings.data();
1226 VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &createInfo, nullptr, &compDSLayout));
1227 }
1228
1229 void buildComputePipeline() {
1230 const std::string spvPath = assetRoot + "/data/" + spvFiles[currentSpvIndex];
1231 const std::vector<char> spv = mxvk::load_spv(spvPath);
1232 VkShaderModule module = mxvk::create_shader_module(device, spv);
1233
1234 VkPushConstantRange pushConstantRange{};
1235 pushConstantRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
1236 pushConstantRange.offset = 0;
1237 pushConstantRange.size = sizeof(ComputePC);
1238
1239 VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
1240 pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
1241 pipelineLayoutInfo.setLayoutCount = 1;
1242 pipelineLayoutInfo.pSetLayouts = &compDSLayout;
1243 pipelineLayoutInfo.pushConstantRangeCount = 1;
1244 pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
1245 VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &compPipeLayout));
1246
1247 VkComputePipelineCreateInfo pipelineInfo{};
1248 pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
1249 pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1250 pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
1251 pipelineInfo.stage.module = module;
1252 pipelineInfo.stage.pName = "main";
1253 pipelineInfo.layout = compPipeLayout;
1254 VK_CHECK_RESULT(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &compPipeline));
1255
1256 vkDestroyShaderModule(device, module, nullptr);
1257 }
1258
1259 void buildDescriptorSets() {
1260 std::array<VkDescriptorPoolSize, 2> poolSizes{};
1261 poolSizes[0] = {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 4};
1262 poolSizes[1] = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 4 * (1 + HISTORY_SIZE)};
1263
1264 VkDescriptorPoolCreateInfo poolInfo{};
1265 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
1266 poolInfo.maxSets = 4;
1267 poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
1268 poolInfo.pPoolSizes = poolSizes.data();
1269 VK_CHECK_RESULT(vkCreateDescriptorPool(device, &poolInfo, nullptr, &compDSPool));
1270
1271 std::array<VkDescriptorSetLayout, 4> layouts{};
1272 layouts.fill(compDSLayout);
1273
1274 VkDescriptorSetAllocateInfo allocInfo{};
1275 allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1276 allocInfo.descriptorPool = compDSPool;
1277 allocInfo.descriptorSetCount = static_cast<uint32_t>(layouts.size());
1278 allocInfo.pSetLayouts = layouts.data();
1279
1280 std::array<VkDescriptorSet, 4> raw{};
1281 VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, raw.data()));
1282 blurDS[0] = raw[0];
1283 blurDS[1] = raw[1];
1284 blendDS[0] = raw[2];
1285 blendDS[1] = raw[3];
1286
1287 writeBlurDS(blurDS[0], workImg[0].view, workImg[1].view);
1288 writeBlurDS(blurDS[1], workImg[1].view, workImg[0].view);
1289 writeBlendDS(blendDS[0], workImg[0].view);
1290 writeBlendDS(blendDS[1], workImg[1].view);
1291 }
1292
1293 void writeBlurDS(VkDescriptorSet descriptorSet, VkImageView destView, VkImageView srcView) {
1294 VkDescriptorImageInfo destInfo{VK_NULL_HANDLE, destView, VK_IMAGE_LAYOUT_GENERAL};
1295 VkDescriptorImageInfo srcInfo{computeSampler, srcView, VK_IMAGE_LAYOUT_GENERAL};
1296 std::vector<VkDescriptorImageInfo> historyInfos(HISTORY_SIZE, VkDescriptorImageInfo{computeSampler, srcView, VK_IMAGE_LAYOUT_GENERAL});
1297
1298 std::array<VkWriteDescriptorSet, 3> writes{};
1299 writes[0] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, descriptorSet, 0, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &destInfo, nullptr, nullptr};
1300 writes[1] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, descriptorSet, 1, 0, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &srcInfo, nullptr, nullptr};
1301 writes[2] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, descriptorSet, 2, 0, HISTORY_SIZE, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, historyInfos.data(), nullptr, nullptr};
1302 vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1303 }
1304
1305 [[nodiscard]] std::vector<char> readDisplayShader(const std::string &name) const {
1306 const std::array<std::string, 3> candidates = {
1307 assetRoot + "/data/" + name,
1308 assetRoot + "/" + name,
1309 std::string("data/") + name,
1310 };
1311
1312 for (const std::string &path : candidates) {
1313 std::ifstream file(path, std::ios::binary);
1314 if (file.is_open()) {
1315 return mxvk::load_spv(path);
1316 }
1317 }
1318
1319 throw mxvk::Exception("Cannot open compute display shader: " + name);
1320 }
1321
1322 void createDisplayBuffers() {
1323 if (displayVertexBuffer != VK_NULL_HANDLE && displayIndexBuffer != VK_NULL_HANDLE) {
1324 return;
1325 }
1326
1327 const std::array<float, 16> vertices = {
1328 0.0f,
1329 0.0f,
1330 0.0f,
1331 0.0f,
1332 1.0f,
1333 0.0f,
1334 1.0f,
1335 0.0f,
1336 1.0f,
1337 1.0f,
1338 1.0f,
1339 1.0f,
1340 0.0f,
1341 1.0f,
1342 0.0f,
1343 1.0f,
1344 };
1345 const std::array<uint16_t, 6> indices = {0, 1, 2, 0, 2, 3};
1346
1347 createBuffer(sizeof(float) * vertices.size(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, displayVertexBuffer, displayVertexMemory);
1348 createBuffer(sizeof(uint16_t) * indices.size(), VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, displayIndexBuffer, displayIndexMemory);
1349
1350 void *mapped = nullptr;
1351 VK_CHECK_RESULT(vkMapMemory(device, displayVertexMemory, 0, sizeof(float) * vertices.size(), 0, &mapped));
1352 std::memcpy(mapped, vertices.data(), sizeof(float) * vertices.size());
1353 vkUnmapMemory(device, displayVertexMemory);
1354
1355 VK_CHECK_RESULT(vkMapMemory(device, displayIndexMemory, 0, sizeof(uint16_t) * indices.size(), 0, &mapped));
1356 std::memcpy(mapped, indices.data(), sizeof(uint16_t) * indices.size());
1357 vkUnmapMemory(device, displayIndexMemory);
1358 }
1359
1360 void createDisplayDescriptorSet() {
1361 VkDescriptorSetLayoutBinding binding{};
1362 binding.binding = 0;
1363 binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1364 binding.descriptorCount = 1;
1365 binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
1366
1367 VkDescriptorSetLayoutCreateInfo layoutInfo{};
1368 layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
1369 layoutInfo.bindingCount = 1;
1370 layoutInfo.pBindings = &binding;
1371 VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &displayDSLayout));
1372
1373 VkDescriptorPoolSize poolSize{};
1374 poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1375 poolSize.descriptorCount = 1;
1376
1377 VkDescriptorPoolCreateInfo poolInfo{};
1378 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
1379 poolInfo.poolSizeCount = 1;
1380 poolInfo.pPoolSizes = &poolSize;
1381 poolInfo.maxSets = 1;
1382 VK_CHECK_RESULT(vkCreateDescriptorPool(device, &poolInfo, nullptr, &displayDSPool));
1383
1384 VkDescriptorSetAllocateInfo allocInfo{};
1385 allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1386 allocInfo.descriptorPool = displayDSPool;
1387 allocInfo.descriptorSetCount = 1;
1388 allocInfo.pSetLayouts = &displayDSLayout;
1389 VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &displayDS));
1390
1391 VkDescriptorImageInfo imageInfo{};
1392 imageInfo.sampler = computeSampler;
1393 imageInfo.imageView = outImg.view;
1394 imageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
1395
1396 VkWriteDescriptorSet write{};
1397 write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1398 write.dstSet = displayDS;
1399 write.dstBinding = 0;
1400 write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1401 write.descriptorCount = 1;
1402 write.pImageInfo = &imageInfo;
1403 vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
1404 }
1405
1406 void rebuildDisplayPipeline() {
1407 if (displayPipeline != VK_NULL_HANDLE) {
1408 vkDestroyPipeline(device, displayPipeline, nullptr);
1409 displayPipeline = VK_NULL_HANDLE;
1410 }
1411 if (displayPipeLayout != VK_NULL_HANDLE) {
1412 vkDestroyPipelineLayout(device, displayPipeLayout, nullptr);
1413 displayPipeLayout = VK_NULL_HANDLE;
1414 }
1415 if (displayDSLayout == VK_NULL_HANDLE || swapchain_format == VK_FORMAT_UNDEFINED) {
1416 return;
1417 }
1418
1419 const VkShaderModule vertModule = mxvk::create_shader_module(device, readDisplayShader("sprite.vert.spv"));
1420 const VkShaderModule fragModule = mxvk::create_shader_module(device, readDisplayShader("sprite.frag.spv"));
1421
1422 VkPipelineShaderStageCreateInfo vertStage{};
1423 vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1424 vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
1425 vertStage.module = vertModule;
1426 vertStage.pName = "main";
1427
1428 VkPipelineShaderStageCreateInfo fragStage{};
1429 fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1430 fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
1431 fragStage.module = fragModule;
1432 fragStage.pName = "main";
1433
1434 const std::array<VkPipelineShaderStageCreateInfo, 2> stages = {vertStage, fragStage};
1435
1436 VkVertexInputBindingDescription binding{};
1437 binding.binding = 0;
1438 binding.stride = sizeof(float) * 4;
1439 binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
1440
1441 std::array<VkVertexInputAttributeDescription, 2> attrs{};
1442 attrs[0].binding = 0;
1443 attrs[0].location = 0;
1444 attrs[0].format = VK_FORMAT_R32G32_SFLOAT;
1445 attrs[0].offset = 0;
1446 attrs[1].binding = 0;
1447 attrs[1].location = 1;
1448 attrs[1].format = VK_FORMAT_R32G32_SFLOAT;
1449 attrs[1].offset = sizeof(float) * 2;
1450
1451 VkPipelineVertexInputStateCreateInfo vertexInput{};
1452 vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
1453 vertexInput.vertexBindingDescriptionCount = 1;
1454 vertexInput.pVertexBindingDescriptions = &binding;
1455 vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size());
1456 vertexInput.pVertexAttributeDescriptions = attrs.data();
1457
1458 VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
1459 inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
1460 inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
1461
1462 const std::array<VkDynamicState, 2> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
1463 VkPipelineDynamicStateCreateInfo dynamicInfo{};
1464 dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
1465 dynamicInfo.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
1466 dynamicInfo.pDynamicStates = dynamicStates.data();
1467
1468 VkPipelineViewportStateCreateInfo viewportState{};
1469 viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
1470 viewportState.viewportCount = 1;
1471 viewportState.scissorCount = 1;
1472
1473 VkPipelineRasterizationStateCreateInfo rasterizer{};
1474 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
1475 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
1476 rasterizer.cullMode = VK_CULL_MODE_NONE;
1477 rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
1478 rasterizer.lineWidth = 1.0f;
1479
1480 VkPipelineMultisampleStateCreateInfo multisample{};
1481 multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
1482 multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
1483
1484 VkPipelineDepthStencilStateCreateInfo depthStencil{};
1485 depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
1486 depthStencil.depthTestEnable = VK_FALSE;
1487 depthStencil.depthWriteEnable = VK_FALSE;
1488
1489 VkPipelineColorBlendAttachmentState blendAttachment{};
1490 blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
1491 blendAttachment.blendEnable = VK_FALSE;
1492
1493 VkPipelineColorBlendStateCreateInfo colorBlend{};
1494 colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
1495 colorBlend.attachmentCount = 1;
1496 colorBlend.pAttachments = &blendAttachment;
1497
1498 VkPushConstantRange pushRange{};
1499 pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
1500 pushRange.size = sizeof(float) * 12;
1501
1502 VkPipelineLayoutCreateInfo layoutInfo{};
1503 layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
1504 layoutInfo.setLayoutCount = 1;
1505 layoutInfo.pSetLayouts = &displayDSLayout;
1506 layoutInfo.pushConstantRangeCount = 1;
1507 layoutInfo.pPushConstantRanges = &pushRange;
1508 VK_CHECK_RESULT(vkCreatePipelineLayout(device, &layoutInfo, nullptr, &displayPipeLayout));
1509
1510 VkPipelineRenderingCreateInfo renderingInfo{};
1511 renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
1512 renderingInfo.colorAttachmentCount = 1;
1513 renderingInfo.pColorAttachmentFormats = &swapchain_format;
1514 if (depth_format != VK_FORMAT_UNDEFINED) {
1515 renderingInfo.depthAttachmentFormat = depth_format;
1516 }
1517
1518 VkGraphicsPipelineCreateInfo pipelineInfo{};
1519 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
1520 pipelineInfo.pNext = &renderingInfo;
1521 pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
1522 pipelineInfo.pStages = stages.data();
1523 pipelineInfo.pVertexInputState = &vertexInput;
1524 pipelineInfo.pInputAssemblyState = &inputAssembly;
1525 pipelineInfo.pViewportState = &viewportState;
1526 pipelineInfo.pRasterizationState = &rasterizer;
1527 pipelineInfo.pMultisampleState = &multisample;
1528 pipelineInfo.pDepthStencilState = &depthStencil;
1529 pipelineInfo.pColorBlendState = &colorBlend;
1530 pipelineInfo.pDynamicState = &dynamicInfo;
1531 pipelineInfo.layout = displayPipeLayout;
1532 pipelineInfo.renderPass = VK_NULL_HANDLE;
1533 VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &displayPipeline));
1534
1535 vkDestroyShaderModule(device, fragModule, nullptr);
1536 vkDestroyShaderModule(device, vertModule, nullptr);
1537 }
1538
1539 void createDisplayResources() {
1540 createDisplayBuffers();
1541 createDisplayDescriptorSet();
1542 rebuildDisplayPipeline();
1543 std::cout << "compute_shader: display path active: Vulkan compute output image -> fullscreen sampled draw (no readback, no sprite upload)\n";
1544 }
1545
1546 void writeBlendDS(VkDescriptorSet descriptorSet, VkImageView srcView) {
1547 VkDescriptorImageInfo destInfo{VK_NULL_HANDLE, outImg.view, VK_IMAGE_LAYOUT_GENERAL};
1548 VkDescriptorImageInfo srcInfo{computeSampler, srcView, VK_IMAGE_LAYOUT_GENERAL};
1549
1550 std::vector<VkDescriptorImageInfo> historyInfos(HISTORY_SIZE);
1551 for (int index = 0; index < HISTORY_SIZE; ++index) {
1552 historyInfos[index] = {computeSampler, histImg[index].view, VK_IMAGE_LAYOUT_GENERAL};
1553 }
1554
1555 std::array<VkWriteDescriptorSet, 3> writes{};
1556 writes[0] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, descriptorSet, 0, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &destInfo, nullptr, nullptr};
1557 writes[1] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, descriptorSet, 1, 0, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &srcInfo, nullptr, nullptr};
1558 writes[2] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, descriptorSet, 2, 0, HISTORY_SIZE, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, historyInfos.data(), nullptr, nullptr};
1559 vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1560 }
1561
1562 void uploadToImage(const void *data, int srcPitch, ComputeImage &img) {
1563 const VkDeviceSize bytes = static_cast<VkDeviceSize>(texWidth) * texHeight * 4;
1564 const int tightPitch = texWidth * 4;
1565 if (data == nullptr || srcPitch < tightPitch) {
1566 return;
1567 }
1568
1569 void *mapped = nullptr;
1570 VK_CHECK_RESULT(vkMapMemory(device, stagingMem, 0, bytes, 0, &mapped));
1571 if (srcPitch == tightPitch) {
1572 std::memcpy(mapped, data, static_cast<size_t>(bytes));
1573 } else {
1574 const auto *src = static_cast<const uint8_t *>(data);
1575 auto *dst = static_cast<uint8_t *>(mapped);
1576 for (int row = 0; row < texHeight; ++row) {
1577 std::memcpy(dst + static_cast<size_t>(row) * tightPitch, src + static_cast<size_t>(row) * srcPitch, static_cast<size_t>(tightPitch));
1578 }
1579 }
1580 vkUnmapMemory(device, stagingMem);
1581
1582 const VkCommandBuffer cmd = beginSingleTimeCommands();
1583
1584 transitionImageLayout(cmd, img.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
1585
1586 VkBufferImageCopy2 region{};
1587 region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2;
1588 region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
1589 region.imageExtent = {static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), 1};
1590
1591 VkCopyBufferToImageInfo2 copyInfo{};
1592 copyInfo.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2;
1593 copyInfo.srcBuffer = stagingBuf;
1594 copyInfo.dstImage = img.image;
1595 copyInfo.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
1596 copyInfo.regionCount = 1;
1597 copyInfo.pRegions = &region;
1598 vkCmdCopyBufferToImage2(cmd, &copyInfo);
1599
1600 transitionImageLayout(cmd, img.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT);
1601
1602 endSingleTimeCommands(cmd);
1603 }
1604
1605#ifdef MXVK_CUDA
1606 bool uploadGpuToImage(const cv::cuda::GpuMat &rgba, cv::cuda::Stream &stream, ComputeImage &img) {
1607 if (rgba.empty() || rgba.type() != CV_8UC4 || rgba.cols != texWidth || rgba.rows != texHeight) {
1608 return false;
1609 }
1610 if (!ensureCudaInterop(img)) {
1611 return false;
1612 }
1613
1614 cudaStream_t cudaStream = mxvk::cuda_stream_handle(stream);
1615 if (!img.cudaUploadLogged) {
1616 std::cout << "compute_shader: CUDA interop upload: copying " << rgba.cols << "x" << rgba.rows << " RGBA GpuMat to Vulkan compute storage image via cudaArray (source pitch=" << static_cast<unsigned long long>(rgba.step) << " bytes, copy row bytes=" << static_cast<unsigned long long>(static_cast<size_t>(rgba.cols) * 4U) << ")\n";
1617 img.cudaUploadLogged = true;
1618 }
1619
1620 cudaError_t cudaResult = cudaMemcpy2DToArrayAsync(img.cudaArray, 0, 0, rgba.ptr(), rgba.step, static_cast<size_t>(rgba.cols) * 4U, static_cast<size_t>(rgba.rows), cudaMemcpyDeviceToDevice, cudaStream);
1621 if (cudaResult != cudaSuccess) {
1622 std::cout << "compute_shader: CUDA interop compute input copy failed: " << cudaGetErrorString(cudaResult) << "\n";
1623 return false;
1624 }
1625
1626 cudaResult = cudaStreamSynchronize(cudaStream);
1627 if (cudaResult != cudaSuccess) {
1628 std::cout << "compute_shader: CUDA interop compute input sync failed: " << cudaGetErrorString(cudaResult) << "\n";
1629 return false;
1630 }
1631
1632 const VkCommandBuffer cmd = beginSingleTimeCommands();
1633 transitionImageLayout(cmd, img.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT);
1634 endSingleTimeCommands(cmd);
1635
1636 if (!img.cudaBarrierLogged) {
1637 std::cout << "compute_shader: CUDA interop sync: CUDA stream synchronized; Vulkan records GENERAL -> GENERAL memory barrier before compute sampling\n";
1638 img.cudaBarrierLogged = true;
1639 }
1640 return true;
1641 }
1642#endif
1643
1644 void dispatchOne(VkCommandBuffer cmd, VkDescriptorSet descriptorSet, int mode) {
1645 ComputePC pc{};
1646 pc.mode = (!spvFiles.empty() && spvFiles[currentSpvIndex] == MODE_SHADER_NAME) ? shaderMode : mode;
1647 pc.historyCount = historyCount;
1648 pc.historyIdx = currentHistIdx;
1649 pc.square_size = currentSquare;
1650 pc.history_dir = currentDir;
1651 pc.alpha = alpha;
1652 pc.do_invert = 0;
1653 pc.do_swap = 0;
1654
1655 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, compPipeline);
1656 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, compPipeLayout, 0, 1, &descriptorSet, 0, nullptr);
1657 vkCmdPushConstants(cmd, compPipeLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc);
1658
1659 const uint32_t groupX = (static_cast<uint32_t>(texWidth) + 15) / 16;
1660 const uint32_t groupY = (static_cast<uint32_t>(texHeight) + 15) / 16;
1661 vkCmdDispatch(cmd, groupX, groupY, 1);
1662 }
1663
1664 void renderComputeOutput(VkCommandBuffer cmd) {
1665 if (displayPipeline == VK_NULL_HANDLE || displayPipeLayout == VK_NULL_HANDLE || displayDS == VK_NULL_HANDLE || displayVertexBuffer == VK_NULL_HANDLE || displayIndexBuffer == VK_NULL_HANDLE) {
1666 return;
1667 }
1668
1669 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, displayPipeline);
1670 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, displayPipeLayout, 0, 1, &displayDS, 0, nullptr);
1671
1672 const VkDeviceSize offset = 0;
1673 vkCmdBindVertexBuffers(cmd, 0, 1, &displayVertexBuffer, &offset);
1674 vkCmdBindIndexBuffer(cmd, displayIndexBuffer, 0, VK_INDEX_TYPE_UINT16);
1675
1676 struct DisplayPC {
1677 float screenWidth;
1678 float screenHeight;
1679 float spritePosX;
1680 float spritePosY;
1681 float spriteSizeW;
1682 float spriteSizeH;
1683 float effectsOn;
1684 float padding2;
1685 float params[4];
1686 } pc{
1687 static_cast<float>(swapchain_extent.width),
1688 static_cast<float>(swapchain_extent.height),
1689 0.0f,
1690 0.0f,
1691 static_cast<float>(swapchain_extent.width),
1692 static_cast<float>(swapchain_extent.height),
1693 0.0f,
1694 0.0f,
1695 {0.0f, 0.0f, 0.0f, 0.0f},
1696 };
1697
1698 vkCmdPushConstants(cmd, displayPipeLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(DisplayPC), &pc);
1699 vkCmdDrawIndexed(cmd, 6, 1, 0, 0, 0);
1700 }
1701
1702 void computeBarrier(VkCommandBuffer cmd, VkImage img) const { transitionImageLayout(cmd, img, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT); }
1703
1704 void runComputeFrame() {
1705 const VkCommandBuffer cmd = beginSingleTimeCommands();
1706 int srcIdx = 0;
1707 int dstIdx = 1;
1708 const bool isModeShader = !spvFiles.empty() && spvFiles[currentSpvIndex] == MODE_SHADER_NAME;
1709
1710 if (isModeShader) {
1711 std::array<VkImageMemoryBarrier2, 2> barriers{};
1712 barriers[0] = makeImageBarrier(workImg[srcIdx].image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
1713
1714 barriers[1] = makeImageBarrier(histImg[historyIndex].image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
1715
1716 VkDependencyInfo dependencyInfo{};
1717 dependencyInfo.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
1718 dependencyInfo.imageMemoryBarrierCount = static_cast<uint32_t>(barriers.size());
1719 dependencyInfo.pImageMemoryBarriers = barriers.data();
1720 vkCmdPipelineBarrier2(cmd, &dependencyInfo);
1721
1722 VkImageCopy2 copy{};
1723 copy.sType = VK_STRUCTURE_TYPE_IMAGE_COPY_2;
1724 copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
1725 copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
1726 copy.extent = {static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), 1};
1727
1728 VkCopyImageInfo2 copyInfo{};
1729 copyInfo.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_INFO_2;
1730 copyInfo.srcImage = workImg[srcIdx].image;
1731 copyInfo.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
1732 copyInfo.dstImage = histImg[historyIndex].image;
1733 copyInfo.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
1734 copyInfo.regionCount = 1;
1735 copyInfo.pRegions = &copy;
1736 vkCmdCopyImage2(cmd, &copyInfo);
1737
1738 barriers[0] = makeImageBarrier(workImg[srcIdx].image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT);
1739
1740 barriers[1] = makeImageBarrier(histImg[historyIndex].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT);
1741
1742 dependencyInfo = {};
1743 dependencyInfo.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
1744 dependencyInfo.imageMemoryBarrierCount = static_cast<uint32_t>(barriers.size());
1745 dependencyInfo.pImageMemoryBarriers = barriers.data();
1746 vkCmdPipelineBarrier2(cmd, &dependencyInfo);
1747
1748 if (historyCount < HISTORY_SIZE) {
1749 ++historyCount;
1750 }
1751 historyIndex = (historyIndex + 1) % HISTORY_SIZE;
1752 dispatchOne(cmd, blendDS[srcIdx], shaderMode);
1753 } else {
1754 const int passes = 3 + (std::rand() % 7);
1755 for (int pass = 0; pass < passes; ++pass) {
1756 dispatchOne(cmd, blurDS[dstIdx], 0);
1757 computeBarrier(cmd, workImg[dstIdx].image);
1758 std::swap(srcIdx, dstIdx);
1759 }
1760
1761 std::array<VkImageMemoryBarrier2, 2> barriers{};
1762 barriers[0] = makeImageBarrier(workImg[srcIdx].image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
1763
1764 barriers[1] = makeImageBarrier(histImg[historyIndex].image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
1765
1766 VkDependencyInfo dependencyInfo{};
1767 dependencyInfo.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
1768 dependencyInfo.imageMemoryBarrierCount = static_cast<uint32_t>(barriers.size());
1769 dependencyInfo.pImageMemoryBarriers = barriers.data();
1770 vkCmdPipelineBarrier2(cmd, &dependencyInfo);
1771
1772 VkImageCopy2 copy{};
1773 copy.sType = VK_STRUCTURE_TYPE_IMAGE_COPY_2;
1774 copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
1775 copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
1776 copy.extent = {static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), 1};
1777
1778 VkCopyImageInfo2 copyInfo{};
1779 copyInfo.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_INFO_2;
1780 copyInfo.srcImage = workImg[srcIdx].image;
1781 copyInfo.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
1782 copyInfo.dstImage = histImg[historyIndex].image;
1783 copyInfo.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
1784 copyInfo.regionCount = 1;
1785 copyInfo.pRegions = &copy;
1786 vkCmdCopyImage2(cmd, &copyInfo);
1787
1788 barriers[0] = makeImageBarrier(workImg[srcIdx].image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT);
1789
1790 barriers[1] = makeImageBarrier(histImg[historyIndex].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT);
1791
1792 dependencyInfo = {};
1793 dependencyInfo.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
1794 dependencyInfo.imageMemoryBarrierCount = static_cast<uint32_t>(barriers.size());
1795 dependencyInfo.pImageMemoryBarriers = barriers.data();
1796 vkCmdPipelineBarrier2(cmd, &dependencyInfo);
1797
1798 if (historyCount < HISTORY_SIZE) {
1799 ++historyCount;
1800 }
1801 historyIndex = (historyIndex + 1) % HISTORY_SIZE;
1802
1803 const bool isMetalMedian = !spvFiles.empty() && spvFiles[currentSpvIndex].find("metalmedianblend") != std::string::npos;
1804 dispatchOne(cmd, blendDS[srcIdx], isMetalMedian ? 2 : 1);
1805 }
1806
1807 transitionImageLayout(cmd, outImg.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
1808
1809 endSingleTimeCommands(cmd);
1810 }
1811
1812 void tickAnimState() {
1813 if (currentDir == 1) {
1814 if (++currentHistIdx >= HISTORY_SIZE - 1) {
1815 currentHistIdx = HISTORY_SIZE - 1;
1816 currentDir = -1;
1817 }
1818 } else if (--currentHistIdx <= 0) {
1819 currentHistIdx = 0;
1820 currentDir = 1;
1821 }
1822
1823 if (squareDir == 1) {
1824 currentSquare += 2;
1825 if (currentSquare >= 64) {
1826 currentSquare = 64;
1827 squareDir = 0;
1828 }
1829 } else {
1830 currentSquare -= 2;
1831 if (currentSquare <= 2) {
1832 currentSquare = 2;
1833 squareDir = 1;
1834 }
1835 }
1836
1837 static int alphaDir = 1;
1838 if (alphaDir == 1) {
1839 alpha += 0.005f;
1840 if (alpha >= (255.0f / 32.0f)) {
1841 alpha = 255.0f / 32.0f;
1842 alphaDir = -1;
1843 }
1844 } else {
1845 alpha -= 0.005f;
1846 if (alpha <= 1.0f) {
1847 alpha = 1.0f;
1848 alphaDir = 1;
1849 }
1850 }
1851 }
1852
1853 static VkImageMemoryBarrier2 makeImageBarrier(VkImage img, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags2 srcStage, VkAccessFlags2 srcAccess, VkPipelineStageFlags2 dstStage, VkAccessFlags2 dstAccess) {
1854 VkImageMemoryBarrier2 barrier{};
1855 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2;
1856 barrier.srcStageMask = srcStage;
1857 barrier.srcAccessMask = srcAccess;
1858 barrier.dstStageMask = dstStage;
1859 barrier.dstAccessMask = dstAccess;
1860 barrier.oldLayout = oldLayout;
1861 barrier.newLayout = newLayout;
1862 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1863 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1864 barrier.image = img;
1865 barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
1866 return barrier;
1867 }
1868
1869 static void transitionImageLayout(VkCommandBuffer cmd, VkImage img, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags2 srcStage, VkAccessFlags2 srcAccess, VkPipelineStageFlags2 dstStage, VkAccessFlags2 dstAccess) {
1870 const VkImageMemoryBarrier2 barrier = makeImageBarrier(img, oldLayout, newLayout, srcStage, srcAccess, dstStage, dstAccess);
1871
1872 VkDependencyInfo dependencyInfo{};
1873 dependencyInfo.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
1874 dependencyInfo.imageMemoryBarrierCount = 1;
1875 dependencyInfo.pImageMemoryBarriers = &barrier;
1876 vkCmdPipelineBarrier2(cmd, &dependencyInfo);
1877 }
1878
1879 void destroyDisplayResources() {
1880 if (displayPipeline != VK_NULL_HANDLE) {
1881 vkDestroyPipeline(device, displayPipeline, nullptr);
1882 displayPipeline = VK_NULL_HANDLE;
1883 }
1884 if (displayPipeLayout != VK_NULL_HANDLE) {
1885 vkDestroyPipelineLayout(device, displayPipeLayout, nullptr);
1886 displayPipeLayout = VK_NULL_HANDLE;
1887 }
1888 if (displayDSPool != VK_NULL_HANDLE) {
1889 vkDestroyDescriptorPool(device, displayDSPool, nullptr);
1890 displayDSPool = VK_NULL_HANDLE;
1891 displayDS = VK_NULL_HANDLE;
1892 }
1893 if (displayDSLayout != VK_NULL_HANDLE) {
1894 vkDestroyDescriptorSetLayout(device, displayDSLayout, nullptr);
1895 displayDSLayout = VK_NULL_HANDLE;
1896 }
1897 if (displayVertexBuffer != VK_NULL_HANDLE) {
1898 vkDestroyBuffer(device, displayVertexBuffer, nullptr);
1899 displayVertexBuffer = VK_NULL_HANDLE;
1900 }
1901 if (displayVertexMemory != VK_NULL_HANDLE) {
1902 vkFreeMemory(device, displayVertexMemory, nullptr);
1903 displayVertexMemory = VK_NULL_HANDLE;
1904 }
1905 if (displayIndexBuffer != VK_NULL_HANDLE) {
1906 vkDestroyBuffer(device, displayIndexBuffer, nullptr);
1907 displayIndexBuffer = VK_NULL_HANDLE;
1908 }
1909 if (displayIndexMemory != VK_NULL_HANDLE) {
1910 vkFreeMemory(device, displayIndexMemory, nullptr);
1911 displayIndexMemory = VK_NULL_HANDLE;
1912 }
1913 }
1914
1915 void destroyComputeResources() {
1916 if (device == VK_NULL_HANDLE) {
1917 return;
1918 }
1919
1920 vkDeviceWaitIdle(device);
1921 destroyDisplayResources();
1922
1923 auto destroyImage = [&](ComputeImage &img) {
1924#ifdef MXVK_CUDA
1925 destroyCudaInterop(img);
1926#endif
1927 if (img.view != VK_NULL_HANDLE) {
1928 vkDestroyImageView(device, img.view, nullptr);
1929 }
1930 if (img.image != VK_NULL_HANDLE) {
1931 vkDestroyImage(device, img.image, nullptr);
1932 }
1933 if (img.memory != VK_NULL_HANDLE) {
1934 vkFreeMemory(device, img.memory, nullptr);
1935 }
1936 img = {};
1937 };
1938
1939 destroyImage(workImg[0]);
1940 destroyImage(workImg[1]);
1941 for (ComputeImage &img : histImg) {
1942 destroyImage(img);
1943 }
1944 destroyImage(outImg);
1945
1946 if (computeSampler != VK_NULL_HANDLE) {
1947 vkDestroySampler(device, computeSampler, nullptr);
1948 computeSampler = VK_NULL_HANDLE;
1949 }
1950 if (compDSPool != VK_NULL_HANDLE) {
1951 vkDestroyDescriptorPool(device, compDSPool, nullptr);
1952 compDSPool = VK_NULL_HANDLE;
1953 }
1954 if (compPipeline != VK_NULL_HANDLE) {
1955 vkDestroyPipeline(device, compPipeline, nullptr);
1956 compPipeline = VK_NULL_HANDLE;
1957 }
1958 if (compPipeLayout != VK_NULL_HANDLE) {
1959 vkDestroyPipelineLayout(device, compPipeLayout, nullptr);
1960 compPipeLayout = VK_NULL_HANDLE;
1961 }
1962 if (compDSLayout != VK_NULL_HANDLE) {
1963 vkDestroyDescriptorSetLayout(device, compDSLayout, nullptr);
1964 compDSLayout = VK_NULL_HANDLE;
1965 }
1966 if (stagingBuf != VK_NULL_HANDLE) {
1967 vkDestroyBuffer(device, stagingBuf, nullptr);
1968 stagingBuf = VK_NULL_HANDLE;
1969 }
1970 if (stagingMem != VK_NULL_HANDLE) {
1971 vkFreeMemory(device, stagingMem, nullptr);
1972 stagingMem = VK_NULL_HANDLE;
1973 }
1974 if (readbackBuf != VK_NULL_HANDLE) {
1975 vkDestroyBuffer(device, readbackBuf, nullptr);
1976 readbackBuf = VK_NULL_HANDLE;
1977 }
1978 if (readbackMem != VK_NULL_HANDLE) {
1979 vkFreeMemory(device, readbackMem, nullptr);
1980 readbackMem = VK_NULL_HANDLE;
1981 }
1982 }
1983};
1984
1985int main(int argc, char **argv) {
1986 std::srand(static_cast<unsigned>(std::time(nullptr)));
1987
1988 try {
1989 Arguments args = proc_args(argc, argv);
1990 if (args.path == ".") {
1992 }
1993
1994 ComputeWindow window(args);
1995 window.loop();
1996 } catch (const mxvk::Exception &e) {
1997 std::cerr << "mxvk: Exception: " << e.text() << "\n";
1998 return EXIT_FAILURE;
1999 } catch (const ArgException<std::string> &e) {
2000 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
2001 return EXIT_FAILURE;
2002 }
2003
2004 return EXIT_SUCCESS;
2005}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
~ComputeWindow() override
Definition main.cpp:65
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition main.cpp:154
void event(SDL_Event &e) override
Handle one SDL event.
Definition main.cpp:156
void proc() override
Execute one processing/update step.
Definition main.cpp:73
ComputeWindow(const Arguments &args)
Definition main.cpp:58
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition main.cpp:152
bool is_hardware_encode() const
True when FFmpeg identifies the active encoder as hardware or hybrid.
Definition mxwrite.hpp:246
bool write_cuda_rgba(void *cuda_rgba_buffer, int src_stride, bool bottom_up=false)
Queue a CUDA RGBA frame for encoding.
Definition mxwrite.cpp:1936
bool open(const std::string &filename, int width, int height, float fps, const char *crf)
Open an output file using the legacy CRF string interface.
Definition mxwrite.cpp:977
void write(void *rgba_buffer)
Queue a host RGBA frame for immediate-mode encoding.
Definition mxwrite.cpp:1751
std::string text() const
void close()
Close the active file and release decoder resources.
double fps() const
Source frame rate, falling back to 30 fps when unknown.
int width() const
Source width in pixels.
int height() const
Source height in pixels.
bool open(const std::string &filename)
Open a video file.
bool readRgba(std::vector< uint8_t > &rgba, int &width, int &height, int &pitch, bool flipY=false)
Decode the next frame as tightly packed RGBA8.
bool using_hardware_decode() const
True when CUDA hardware decode is active.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkSwapchainKHR swapchain
Definition mxvk.hpp:613
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
VkDevice device
Definition mxvk.hpp:606
VkFormat swapchain_format
Definition mxvk.hpp:614
VkExtent2D swapchain_extent
Definition mxvk.hpp:616
VkFormat depth_format
Definition mxvk.hpp:615
void createDevice()
Create final device resources.
Definition mxvk.cpp:2028
void clearTextQueue()
Clear all queued text draw calls for the current frame.
Definition mxvk.cpp:3964
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:192
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VkCommandPool command_pool
Definition mxvk.hpp:627
VK_Window()=default
Construct an empty window object.
VkPhysicalDevice physical_device
Definition mxvk.hpp:605
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3919
VkQueue graphics_queue
Definition mxvk.hpp:609
#define compute_shader_ASSET_DIR
Definition main.cpp:36
#define MXVK_VALIDATION
Definition mxvk.hpp:28
OpenCV video-capture integration for the Vulkan backend.
FFmpeg video-file capture with optional CUDA hardware decoding.
Small compatibility wrappers around OpenCV CUDA APIs.
#define VK_CHECK_RESULT(f)
FFmpeg-based video writer used by MXWrite.
int main()
Definition main.py:165
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
VkShaderModule create_shader_module(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
std::vector< char > load_spv(const std::string &path)
Load a SPIR-V file from disk.
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720
int32_t mode
Definition main.cpp:46
int32_t do_swap
Definition main.cpp:52
int32_t historyIdx
Definition main.cpp:48
int32_t history_dir
Definition main.cpp:50
int32_t historyCount
Definition main.cpp:47
float alpha
Definition main.cpp:51
int32_t square_size
Definition main.cpp:49
int32_t do_invert
Definition main.cpp:53
std::string codec
Encoder selection policy or exact FFmpeg encoder name.
Definition mxwrite.hpp:99
std::string tune
Optional tuning mode.
Definition mxwrite.hpp:96
bool realtime
Enable low-latency settings.
Definition mxwrite.hpp:101
int crf
Constant Rate Factor.
Definition mxwrite.hpp:97
bool block_when_full
Pace producers to encoder throughput instead of dropping frames.
Definition mxwrite.hpp:102
std::string preset
Encoder preset name.
Definition mxwrite.hpp:95