ACMX 2.136.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
ACMXVK/main_window.cpp
Go to the documentation of this file.
1#include "main_window.hpp"
2
3#include <csignal>
4
5namespace acmxvk {
6 namespace {
7 volatile std::sig_atomic_t HEADLESS_SHUTDOWN_REQUESTED = 0;
8 constexpr int COLOR_TRANSFER_SMPTE2084 = 16;
9 constexpr int COLOR_TRANSFER_ARIB_STD_B67 = 18;
10
11 [[nodiscard]] float decode_pq(float encoded) {
12 constexpr float M1 = 2610.0F / 16384.0F;
13 constexpr float M2 = 2523.0F / 32.0F;
14 constexpr float C1 = 3424.0F / 4096.0F;
15 constexpr float C2 = 2413.0F / 128.0F;
16 constexpr float C3 = 2392.0F / 128.0F;
17 const float power_value =
18 std::pow(std::clamp(encoded, 0.0F, 1.0F), 1.0F / M2);
19 const float numerator = std::max(power_value - C1, 0.0F);
20 const float denominator =
21 std::max(C2 - C3 * power_value, 1.0e-6F);
22 return std::pow(numerator / denominator, 1.0F / M1);
23 }
24
25 [[nodiscard]] float decode_hlg(float encoded) {
26 constexpr float A = 0.17883277F;
27 constexpr float B = 0.28466892F;
28 constexpr float C = 0.55991073F;
29 encoded = std::max(encoded, 0.0F);
30 if (encoded <= 0.5F) {
31 return encoded * encoded / 3.0F;
32 }
33 return (std::exp((encoded - C) / A) + B) / 12.0F;
34 }
35
36 [[nodiscard]] cv::Mat decode_hdr_transfer(const cv::Mat &rgba,
37 bool hlg) {
38 if (rgba.empty() ||
39 (rgba.type() != CV_8UC4 && rgba.type() != CV_16UC4)) {
40 return rgba;
41 }
42 cv::Mat linear(rgba.rows, rgba.cols, CV_16UC4);
43 const float scale = rgba.type() == CV_16UC4
44 ? 1.0F / 65535.0F
45 : 1.0F / 255.0F;
46 for (int row = 0; row < rgba.rows; ++row) {
47 auto *destination = linear.ptr<std::uint16_t>(row);
48 for (int column = 0; column < rgba.cols; ++column) {
49 for (int channel = 0; channel < 3; ++channel) {
50 const std::size_t offset =
51 static_cast<std::size_t>(column) * 4U +
52 static_cast<std::size_t>(channel);
53 const float encoded =
54 rgba.type() == CV_16UC4
55 ? static_cast<float>(
56 rgba.ptr<std::uint16_t>(row)[offset]) *
57 scale
58 : static_cast<float>(
59 rgba.ptr<std::uint8_t>(row)[offset]) *
60 scale;
61 const float decoded =
62 hlg ? decode_hlg(encoded) : decode_pq(encoded);
63 destination[offset] = static_cast<std::uint16_t>(
64 std::lround(std::clamp(decoded, 0.0F, 1.0F) *
65 65535.0F));
66 }
67 const std::size_t alpha_offset =
68 static_cast<std::size_t>(column) * 4U + 3U;
69 destination[alpha_offset] =
70 rgba.type() == CV_16UC4
71 ? rgba.ptr<std::uint16_t>(row)[alpha_offset]
72 : static_cast<std::uint16_t>(
73 rgba.ptr<std::uint8_t>(row)[alpha_offset] *
74 257U);
75 }
76 }
77 return linear;
78 }
79
80 [[nodiscard]] cv::Mat rgba16ToRgba8(const cv::Mat &rgba) {
81 if (rgba.empty() || rgba.type() != CV_16UC4) {
82 return rgba;
83 }
84 cv::Mat converted;
85 rgba.convertTo(converted, CV_8UC4, 1.0 / 257.0);
86 return converted;
87 }
88
89 [[nodiscard]] float tone_map_channel(float value) {
90 value = std::max(value, 0.0F);
91 return std::clamp(
92 (value * (2.51F * value + 0.03F)) /
93 (value * (2.43F * value + 0.59F) + 0.14F),
94 0.0F, 1.0F);
95 }
96
97 [[nodiscard]] float encode_srgb(float value) {
98 value = std::clamp(value, 0.0F, 1.0F);
99 return value <= 0.0031308F
100 ? 12.92F * value
101 : 1.055F * std::pow(value, 1.0F / 2.4F) - 0.055F;
102 }
103
104 [[nodiscard]] std::vector<std::uint8_t> tone_map_hdr_rgba16(
105 const std::vector<std::uint16_t> &rgba, bool hlg) {
106 std::vector<std::uint8_t> converted(rgba.size());
107 const float reference_scale =
108 hlg ? 1000.0F / 203.0F : 10000.0F / 203.0F;
109 for (std::size_t offset = 0; offset + 3U < rgba.size();
110 offset += 4U) {
111 const auto decode = [hlg](std::uint16_t sample) {
112 const float encoded =
113 static_cast<float>(sample) / 65535.0F;
114 return hlg ? decode_hlg(encoded) : decode_pq(encoded);
115 };
116 const float red = decode(rgba[offset]) * reference_scale;
117 const float green =
118 decode(rgba[offset + 1U]) * reference_scale;
119 const float blue =
120 decode(rgba[offset + 2U]) * reference_scale;
121 const float bt709_red =
122 1.660491F * red - 0.587641F * green - 0.072850F * blue;
123 const float bt709_green =
124 -0.124550F * red + 1.132900F * green - 0.008349F * blue;
125 const float bt709_blue =
126 -0.018151F * red - 0.100579F * green + 1.118730F * blue;
127 converted[offset] = static_cast<std::uint8_t>(std::lround(
128 encode_srgb(tone_map_channel(bt709_red)) * 255.0F));
129 converted[offset + 1U] = static_cast<std::uint8_t>(
130 std::lround(encode_srgb(tone_map_channel(bt709_green)) *
131 255.0F));
132 converted[offset + 2U] = static_cast<std::uint8_t>(
133 std::lround(encode_srgb(tone_map_channel(bt709_blue)) *
134 255.0F));
135 converted[offset + 3U] = static_cast<std::uint8_t>(
136 (static_cast<std::uint32_t>(rgba[offset + 3U]) + 128U) /
137 257U);
138 }
139 return converted;
140 }
141 } // namespace
142
143 void request_headless_shutdown([[maybe_unused]] int signal_number) noexcept {
144 HEADLESS_SHUTDOWN_REQUESTED = 1;
145 }
146
147 // Window construction, event handling, rendering callbacks, and main loop.
149 : mxvk::VK_Window("ACMXVK", options.width, options.height,
150 options.fullscreen, MXVK_VALIDATION,
151 options.enable_vsync
152 ? PresentModePreference::Vsync
153 : PresentModePreference::LowLatency,
154 options.headless ? RuntimeMode::Headless
155 : RuntimeMode::Windowed),
156 options(std::move(options)) {
157 if (this->options.headless) {
158 std::cout << "acmxvk: headless mode enabled: surface-free Vulkan "
159 "rendering without an SDL window\n";
160 }
161 setClearColor(0.0F, 0.0F, 0.0F, 1.0F);
162 setEnableScreenshot(this->options.enable_screenshot);
167 openAudio();
168 loadShaders();
172 openMidi();
173 loadPlaylist();
175 openInput();
180 openOutput();
181 updateWindowTitle(true);
182 }
183
187 try {
188 flushFrameReadbacks();
189 } catch (const std::exception &error) {
190 std::cerr << "acmxvk: unable to flush pending frame readbacks: "
191 << error.what() << '\n';
192 }
195 }
196 if (model_initialized && getDevice() != VK_NULL_HANDLE) {
197 vkDeviceWaitIdle(getDevice());
198 input_model.cleanup(this);
199 model_initialized = false;
200 std::cout << "acmxvk: released 3D model resources\n";
201 }
202 const bool should_copy_audio =
204#ifdef AUDIO_ENABLED
205 const bool should_mux_file_audio =
206 file_audio_source != nullptr && writer.is_open() &&
207 !options.output_file.empty() && !options.png_output &&
209 const bool should_mux_live_audio =
210 audio_engine != nullptr && file_audio_source == nullptr &&
211 audio_engine->is_recording() && writer.is_open() &&
212 !options.output_file.empty() && !options.png_output &&
215 const bool should_write_live_audio =
216 audio_engine != nullptr && audio_engine->is_recording() &&
217 !options.record_audio_file.empty();
218 audio::AudioRecording live_audio_recording;
219 if (audio_engine != nullptr && audio_engine->is_recording()) {
220 live_audio_recording = audio_engine->stop_recording();
221 }
222 if (file_audio_source != nullptr) {
223 file_audio_source->stop_output();
224 }
225#endif
226 if (writer.is_open()) {
227 writer.close();
228 std::cout << "acmxvk: recording closed after " << output_frame_count
229 << " frames\n";
230 }
231 if (options.png_output) {
232 std::cout << "acmxvk: PNG sequence closed after " << png_frame_count
233 << " frames\n";
234 }
235 if (options.generate_interval > 0) {
236 std::cout << "acmxvk: generated " << generated_frame_count
237 << " periodic PNG frames\n";
238 }
239 if (capture.is_open()) {
240 capture.close();
241 }
242#ifdef MXVK_WITH_FFMPEG_CAPTURE
243 if (ffmpeg_capture.is_open()) {
244 ffmpeg_capture.close();
245 }
246#endif
247 if (should_copy_audio) {
249 std::cout << "acmxvk: copied audio track from " << options.input_file
250 << " to " << options.output_file << '\n';
251 }
252#ifdef AUDIO_ENABLED
253 if (should_mux_file_audio) {
254 const double video_duration = writer.get_duration();
255 if (!file_audio_source->mux_into_video(options.output_file,
256 video_duration)) {
257 std::cerr << "acmxvk: file-audio mux failed; preserving the "
258 "encoded video without audio\n";
259 }
260 }
261 if (should_write_live_audio) {
262 if (live_audio_recording.empty()) {
263 std::cerr << "acmxvk: standalone audio recording was empty; "
264 "no WAV file was written\n";
265 } else if (!audio::write_wav_file(live_audio_recording,
267 std::cerr << "acmxvk: could not write WAV recording: "
268 << options.record_audio_file << '\n';
269 } else {
270 std::cout << "acmxvk: wrote "
271 << live_audio_recording.duration_seconds()
272 << " seconds of microphone audio to "
273 << options.record_audio_file << '\n';
274 }
275 }
276 if (should_mux_live_audio) {
277 const double video_duration = writer.get_duration();
278 if (live_audio_recording.empty()) {
279 std::cerr << "acmxvk: live audio recording was empty; preserving "
280 "the encoded video without audio\n";
282 std::move(live_audio_recording.samples),
283 live_audio_recording.sample_rate,
284 options.output_file, video_duration)) {
285 std::cerr << "acmxvk: live-audio mux failed; preserving the "
286 "encoded video without audio\n";
287 }
288 }
289#endif
291 }
292
293 void MainWindow::event(SDL_Event &event) {
294 mxvk::VK_Window::event(event);
295 if (event.type == SDL_EVENT_KEY_DOWN &&
296 event.key.key == SDLK_PAGEUP) {
297 adjustTimeSpeed(0.1);
298 } else if (event.type == SDL_EVENT_KEY_DOWN &&
299 event.key.key == SDLK_PAGEDOWN) {
300 adjustTimeSpeed(-0.1);
301 }
302
303 if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
304 switch (event.key.key) {
305 case SDLK_UP:
306 if ((event.key.mod & SDL_KMOD_SHIFT) != 0 || !playlist_enabled) {
307 selectShader(-1);
308 } else {
310 }
311 break;
312 case SDLK_DOWN:
313 if ((event.key.mod & SDL_KMOD_SHIFT) != 0 || !playlist_enabled) {
314 selectShader(1);
315 } else {
317 }
318 break;
319 case SDLK_LEFT:
320 selectGpuFilter(-1);
321 break;
322 case SDLK_RIGHT:
324 break;
325 case SDLK_SPACE:
329 std::cout << "acmxvk: shader effects "
330 << (effects_enabled ? "enabled" : "bypassed") << '\n';
331 break;
332 case SDLK_P:
333 if (!playlist.empty()) {
337 std::cout << "acmxvk: playlist "
338 << (playlist_enabled ? "enabled" : "disabled") << '\n';
339 if (playlist_enabled) {
340 logSelectedPlaylistNode("selected");
341 }
342 } else {
343 togglePause();
344 }
345 break;
346 case SDLK_L:
347 toggleFreeze();
348 break;
349 case SDLK_T:
351 previous_frame = std::chrono::steady_clock::now();
352 std::cout << "acmxvk: shader time "
353 << (shader_time_active ? "enabled" : "disabled") << '\n';
354 break;
355 case SDLK_Q:
356#ifdef AUDIO_ENABLED
357 if (audioSourceOpen()) {
359 previous_frame = std::chrono::steady_clock::now();
360 std::cout << "acmxvk: audio-reactive shader time "
361 << (audio_time_active ? "enabled" : "disabled")
362 << '\n';
363 }
364#endif
365 break;
366 case SDLK_HOME:
367#ifdef AUDIO_ENABLED
368 if (audioSourceOpen()) {
370 std::cout << "acmxvk: audio delta-time scaling "
371 << (audio_delta_time ? "enabled" : "disabled")
372 << '\n';
373 }
374#endif
375 break;
376 case SDLK_END:
377#ifdef AUDIO_ENABLED
378 if (audioSourceOpen()) {
381 std::cout << "acmxvk: spectrum sensitivity scaling "
382 << (spectrum_scale_by_sensitivity ? "enabled"
383 : "disabled")
384 << '\n';
385 }
386#endif
387 break;
388 case SDLK_U:
389 stepShaderTime(0.05);
390 break;
391 case SDLK_I:
392 stepShaderTime(-0.05);
393 break;
394 case SDLK_F:
396 break;
397 case SDLK_F9:
400 hud_fps_last_tick = std::chrono::steady_clock::now();
401 if (!counter_disabled) {
403 }
404 std::cout << "acmxvk: runtime HUD "
405 << (counter_disabled ? "hidden" : "shown")
406 << " (F9)\n";
407 break;
408 case SDLK_E:
409 if (!options.watermark_text.empty()) {
411 std::cout << "acmxvk: watermark "
412 << (watermark_enabled ? "enabled" : "disabled")
413 << '\n';
414 }
415 break;
416 case SDLK_INSERT:
418 break;
419 case SDLK_DELETE:
421 break;
422 case SDLK_M:
423 if (!configured_passes.empty()) {
427 std::cout << "acmxvk: multipass "
428 << (multipass_enabled ? "enabled" : "disabled") << '\n';
429 }
430 break;
431 case SDLK_J:
432 toggleAutopilot(false);
433 break;
434 case SDLK_N:
437 std::cout << "acmxvk: random autopilot crossfade "
438 << (autopilot_random_crossfade ? "enabled"
439 : "disabled")
440 << '\n';
441 break;
442 case SDLK_K:
444 std::cout << "acmxvk: shader lock "
445 << (shader_locked ? "enabled" : "disabled")
446 << '\n';
447 break;
448 case SDLK_3:
449 if (model_initialized) {
453 std::chrono::steady_clock::now();
455 std::cout << "acmxvk: "
456 << (model_3d_active ? "3D model" : "2D sprite")
457 << " rendering enabled\n";
458 }
459 break;
460 case SDLK_V:
461 if (model_initialized) {
463 std::cout << "acmxvk: 3D view rotation "
464 << (model_auto_rotate ? "enabled" : "disabled")
465 << '\n';
466 }
467 break;
468 case SDLK_C:
469 if (model_initialized) {
471 std::cout << "acmxvk: 3D wave effect "
472 << (model_wave_active ? "enabled"
473 : "disabled")
474 << '\n';
475 }
476 break;
477 case SDLK_O:
478 if (model_initialized) {
481 std::cout << "acmxvk: 3D scale oscillation "
483 ? "enabled"
484 : "disabled")
485 << '\n';
486 }
487 break;
488 case SDLK_X:
489 if (model_initialized) {
490 model_pitch_degrees = 0.0F;
491 model_yaw_degrees = 270.0F;
496 model_scale = 1.0F;
498 std::cout << "acmxvk: model view reset\n";
499 }
500 break;
501 case SDLK_LEFTBRACKET:
502 cycleCrossfade(-1);
503 break;
504 case SDLK_RIGHTBRACKET:
506 break;
507 case SDLK_MINUS:
508 case SDLK_UNDERSCORE:
509 case SDLK_KP_MINUS:
510 if ((event.key.mod & SDL_KMOD_SHIFT) != 0) {
511 adjustModelScale(-0.05F);
512 }
513 break;
514 case SDLK_PLUS:
515 case SDLK_EQUALS:
516 case SDLK_KP_PLUS:
517 if ((event.key.mod & SDL_KMOD_SHIFT) != 0) {
518 adjustModelScale(0.05F);
519 }
520 break;
521 case SDLK_COMMA:
522 if (model_initialized) {
524 std::max(0.0F, model_rotation_speed - 5.0F);
525 std::cout << "acmxvk: 3D view rotation speed "
526 << model_rotation_speed << " degrees/second\n";
527 }
528 break;
529 case SDLK_PERIOD:
530 if (model_initialized) {
532 std::min(360.0F, model_rotation_speed + 5.0F);
533 std::cout << "acmxvk: 3D view rotation speed "
534 << model_rotation_speed << " degrees/second\n";
535 }
536 break;
537 case SDLK_Y:
538 toggleAutopilot(true);
539 break;
540 case SDLK_Z:
542 break;
543 case SDLK_4:
545 break;
546 case SDLK_5:
548 break;
549 case SDLK_6:
551 break;
552 default:
553 break;
554 }
555 } else if (event.type == SDL_EVENT_MOUSE_MOTION) {
556 mouse_x = event.motion.x;
557 mouse_y = event.motion.y;
559 const int x = static_cast<int>(event.motion.x);
560 const int y = static_cast<int>(event.motion.y);
562 static_cast<float>(x - model_last_mouse_x) * 0.35F;
563 model_pitch_degrees = std::clamp(
565 static_cast<float>(y - model_last_mouse_y) * 0.35F,
566 -89.0F, 89.0F);
569 }
570 } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
571 event.button.button == SDL_BUTTON_LEFT) {
572 mouse_pressed = true;
573 mouse_x = event.button.x;
574 mouse_y = event.button.y;
576 model_last_mouse_x = static_cast<int>(event.button.x);
577 model_last_mouse_y = static_cast<int>(event.button.y);
578 } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP &&
579 event.button.button == SDL_BUTTON_LEFT) {
580 mouse_pressed = false;
581 model_mouse_dragging = false;
582 mouse_x = event.button.x;
583 mouse_y = event.button.y;
584 } else if (event.type == SDL_EVENT_MOUSE_WHEEL &&
587 const float wheel = event.wheel.y != 0.0F
588 ? event.wheel.y
589 : static_cast<float>(
590 event.wheel.integer_y);
591 model_camera_distance = std::clamp(
592 model_camera_distance - wheel * 0.2F, -20.0F, 20.0F);
593 }
594 }
595
597 // Initial headless extent configuration creates MXVK's surface-free
598 // targets before MainWindow performs its normal sprite setup. Do not
599 // initialize here: doing so consumes video frame zero, after which the
600 // constructor consumes frame one and leaves an empty encoded PTS zero.
601 if (frame_sprite == nullptr) {
602 return;
603 }
605 if (model_initialized) {
606 input_model.resize(this);
607 }
609 }
610
611 void MainWindow::onRecordCustomRendering(VkCommandBuffer command_buffer,
612 std::uint32_t image_index) {
614 return;
615 }
616 recordModel(command_buffer, image_index, VK_NULL_HANDLE);
617 }
618
620 VkCommandBuffer command_buffer, std::uint32_t image_index,
621 VkImageView texture_view,
622 [[maybe_unused]] VkExtent2D texture_extent) {
624 return;
625 }
626 recordModel(command_buffer, image_index, texture_view);
627 }
628
629 void MainWindow::recordModel(VkCommandBuffer command_buffer,
630 std::uint32_t image_index,
631 VkImageView texture_view) {
633 return;
634 }
635
636 const auto now = std::chrono::steady_clock::now();
637 float delta = std::chrono::duration<float>(
639 .count();
641 delta = std::clamp(delta, 0.0F, 0.1F);
642
643 float animation_delta = delta;
644 std::uint64_t animation_steps = 1U;
645 double video_timeline = 0.0;
646 std::uint64_t video_frame_index = 0U;
647 if (currentVideoTimeline(video_timeline, &video_frame_index)) {
649 video_frame_index < previous_model_video_frame) {
651 video_frame_index < previous_model_video_frame) {
652 model_wave_phase = 0.0F;
660 if (model_auto_rotate) {
662 }
663 }
664 animation_delta = 0.0F;
665 animation_steps = 0U;
667 } else {
668 animation_steps =
669 video_frame_index - previous_model_video_frame;
670 animation_delta = static_cast<float>(
671 static_cast<double>(animation_steps) /
673 }
674 previous_model_video_frame = video_frame_index;
675 } else {
677 }
679 model_view_rotation_degrees = std::fmod(
681 model_rotation_speed * animation_delta,
682 360.0F);
683 }
684 if (model_wave_active) {
685 const float wave_step =
688 : 0.05F;
689 model_wave_phase = std::fmod(
691 wave_step * static_cast<float>(animation_steps),
692 360.0F);
693
694 const auto advance_amplitude = [](float &amplitude,
695 float &direction,
696 std::uint64_t steps) {
697 constexpr float AMPLITUDE_RANGE = 0.5F;
698 constexpr float AMPLITUDE_PERIOD =
699 AMPLITUDE_RANGE * 2.0F;
700 float phase = direction >= 0.0F
701 ? amplitude
702 : AMPLITUDE_PERIOD - amplitude;
703 phase = std::fmod(
704 phase + 0.005F * static_cast<float>(steps),
705 AMPLITUDE_PERIOD);
706 if (phase < AMPLITUDE_RANGE) {
707 amplitude = phase;
708 direction = 1.0F;
709 } else {
710 amplitude = AMPLITUDE_PERIOD - phase;
711 direction = -1.0F;
712 }
713 };
714 advance_amplitude(model_wave_amplitude_x,
715 model_wave_direction_x, animation_steps);
716 advance_amplitude(model_wave_amplitude_y,
717 model_wave_direction_y, animation_steps);
718 advance_amplitude(model_wave_amplitude_z,
719 model_wave_direction_z, animation_steps);
720 }
724 0.016F * static_cast<float>(animation_steps),
725 2.0F * std::numbers::pi_v<float>);
726 }
727
728 const bool *keyboard = SDL_GetKeyboardState(nullptr);
729 const bool model_scale_modifier =
730 (SDL_GetModState() & SDL_KMOD_SHIFT) != 0;
732 keyboard[SDL_SCANCODE_1]) {
733 model_camera_movement_speed = std::clamp(
734 model_camera_movement_speed + 0.1F * delta * 30.0F,
735 0.01F, 20.0F);
736 }
738 keyboard[SDL_SCANCODE_2]) {
739 model_camera_movement_speed = std::clamp(
740 model_camera_movement_speed - 0.1F * delta * 30.0F,
741 0.01F, 20.0F);
742 }
743 if (!model_scale_oscillation_active && !model_scale_modifier &&
744 (keyboard[SDL_SCANCODE_EQUALS] ||
745 keyboard[SDL_SCANCODE_KP_PLUS])) {
746 model_camera_distance = std::clamp(
749 -20.0F, 20.0F);
750 }
751 if (!model_scale_oscillation_active && !model_scale_modifier &&
752 (keyboard[SDL_SCANCODE_MINUS] ||
753 keyboard[SDL_SCANCODE_KP_MINUS])) {
754 model_camera_distance = std::clamp(
757 -20.0F, 20.0F);
758 }
759 if (!model_auto_rotate) {
760 if (keyboard[SDL_SCANCODE_W]) {
762 model_camera_rotation_speed * 0.3F * delta * 30.0F;
763 }
764 if (keyboard[SDL_SCANCODE_S]) {
766 model_camera_rotation_speed * 0.33F * delta * 30.0F;
767 }
769 std::fmod(model_pitch_degrees, 360.0F);
770 if (model_pitch_degrees < 0.0F) {
771 model_pitch_degrees += 360.0F;
772 }
773 if (keyboard[SDL_SCANCODE_A]) {
775 model_camera_rotation_speed * 0.3F * delta * 30.0F;
776 }
777 if (keyboard[SDL_SCANCODE_D]) {
779 model_camera_rotation_speed * 0.3F * delta * 30.0F;
780 }
781 model_yaw_degrees = std::fmod(model_yaw_degrees, 360.0F);
782 if (model_yaw_degrees < 0.0F) {
783 model_yaw_degrees += 360.0F;
784 }
785 }
786
787 const VkExtent2D extent = getRenderExtent();
788 const float aspect = extent.height > 0U
789 ? static_cast<float>(extent.width) /
790 static_cast<float>(extent.height)
791 : 1.0F;
792
793 mxvk::UniformBufferObject uniforms{};
794 uniforms.model = glm::scale(
795 glm::mat4(1.0F),
796 glm::vec3(input_model.modelRenderScale() * model_scale));
797 uniforms.model = glm::rotate(
798 uniforms.model, glm::radians(model_rotation_x_degrees),
799 glm::vec3(1.0F, 0.0F, 0.0F));
800 uniforms.model = glm::rotate(
801 uniforms.model, glm::radians(model_rotation_y_degrees),
802 glm::vec3(0.0F, 1.0F, 0.0F));
803 uniforms.model = glm::rotate(
804 uniforms.model, glm::radians(model_rotation_z_degrees),
805 glm::vec3(0.0F, 0.0F, 1.0F));
806 uniforms.model = glm::translate(
807 uniforms.model, input_model.modelCenterOffset());
808
809 glm::vec3 look_direction{};
810 glm::vec3 camera_up(0.0F, 1.0F, 0.0F);
811 if (model_auto_rotate) {
812 const float rotation =
813 glm::radians(model_view_rotation_degrees);
814 look_direction = glm::vec3(
815 0.48F * std::sin(rotation),
816 0.48F * std::sin(rotation * 0.7F),
817 0.48F * std::cos(rotation));
818 } else {
819 const float pitch = glm::radians(model_pitch_degrees);
820 const float yaw = glm::radians(model_yaw_degrees);
821 look_direction = glm::normalize(glm::vec3(
822 std::cos(pitch) * std::cos(yaw),
823 std::sin(pitch),
824 std::cos(pitch) * std::sin(yaw))) *
825 0.48F;
826 camera_up = glm::vec3(-std::sin(pitch) * std::cos(yaw),
827 std::cos(pitch),
828 -std::sin(pitch) * std::sin(yaw));
829 }
830 const float camera_offset =
832 ? 0.3F * std::sin(model_scale_oscillation_phase)
834 const glm::vec3 camera_position =
835 -glm::normalize(look_direction) * camera_offset;
836 uniforms.view = glm::lookAt(camera_position,
837 camera_position + look_direction,
838 camera_up);
839 uniforms.proj = glm::perspective(
840 glm::radians(120.0F), aspect, 0.01F, 1000.0F);
841 uniforms.proj[1][1] *= -1.0F;
842 uniforms.fx =
844 ? glm::vec4(model_wave_amplitude_x,
847 : glm::vec4(0.0F);
848
849 input_model.updateFragmentUBO(image_index,
851
852 mxvk::ModelFragmentPushConstants fragment_constants{};
853 fragment_constants.screenWidth = static_cast<float>(extent.width);
854 fragment_constants.screenHeight = static_cast<float>(extent.height);
855 fragment_constants.spriteSizeW = static_cast<float>(extent.width);
856 fragment_constants.spriteSizeH = static_cast<float>(extent.height);
857 fragment_constants.effectsOn = effects_enabled ? 1.0F : 0.0F;
858 fragment_constants.params = glm::vec4(
859 1.0F, 1.0F, 1.0F, static_cast<float>(shader_time));
860 input_model.setFragmentPushConstants(fragment_constants);
861
862 if (texture_view != VK_NULL_HANDLE) {
863 input_model.renderWithExternalTexture(
864 command_buffer, image_index, texture_view, uniforms,
865 false);
866 } else {
867 input_model.renderWithPushConstants(
868 command_buffer, image_index, 0U, uniforms, false);
869 }
870 }
871
873 if (options.headless && HEADLESS_SHUTDOWN_REQUESTED != 0) {
875 std::cout << "acmxvk: Ctrl+C received; draining rendered "
876 "frames and closing output\n";
878 }
879 setFrameReadbackEnabled(false);
880 exit();
881 return;
882 }
883 if (recording_complete) {
884 return;
885 }
886
888
889 pollMidi();
891
892 source_frame_received = false;
893 recording_frame_due = false;
895 bool clocked_video_handled = false;
896
900 } else if (initial_frame_pending) {
901 initial_frame_pending = false;
903 } else {
904 double clock_seconds = 0.0;
907 mediaClockSeconds(clock_seconds)) {
908 clocked_video_handled = true;
909 if (!readClockedVideoFrame(clock_seconds)) {
910 return;
911 }
912 } else {
913 const bool read_frame = readTrackedInputFrame();
914 if (!read_frame && !handleCaptureEnd()) {
915 return;
916 }
918 read_frame || source_kind == SourceKind::Video;
919 }
920 }
921 }
922
924 const bool render_latest_camera_frame =
927 if ((source_frame_received || render_latest_camera_frame) &&
928 !clocked_video_handled) {
929 recording_frame_due = true;
933 } else {
934 double clock_seconds = 0.0;
935 if (mediaClockSeconds(clock_seconds)) {
936 const double rate = outputFrameRate();
937 const std::uint64_t target_frame =
938 static_cast<std::uint64_t>(std::floor(
939 std::max(clock_seconds, 0.0) * rate));
940 if (target_frame < next_clock_output_frame) {
941 recording_frame_due = false;
942 } else {
944 recording_frame_pts = target_frame;
945 next_clock_output_frame = target_frame + 1;
947 writer.is_open() &&
949 std::cout
950 << "acmxvk: camera recording uses real-time "
951 "PTS; slow frames preserve capture duration\n";
953 }
954 }
955 }
956 }
957 }
958
959 if (!rendering_frozen) {
961 }
963 const VkExtent2D extent = getRenderExtent();
964 const int target_width = extent.width > 0U ? static_cast<int>(extent.width) : options.width;
965 const int target_height =
966 extent.height > 0U ? static_cast<int>(extent.height) : options.height;
967
968 if (!rendering_frozen) {
969 updateShaderUniforms(target_width, target_height);
970 }
972 frame_sprite->drawSpriteRect(0, 0, target_width,
973 target_height);
974 }
977 setFrameReadbackEnabled(
980 }
981#ifdef AUDIO_ENABLED
982
983 // Audio, MIDI, custom controls, and media-clock coordination.
986 audio_warmup_started = false;
987 if (options.audio_warm_rate <= 0.0) {
988 std::cout << "acmxvk: audio shader warmup disabled\n";
989 } else {
990 std::cout << "acmxvk: audio shader warmup "
991 << options.audio_warm_rate << "/second (~"
992 << 1.0 / options.audio_warm_rate
993 << " seconds to full strength)\n";
994 }
995 }
996
997 [[nodiscard]] float MainWindow::updateAudioWarmup(
998 std::chrono::steady_clock::time_point now) {
999 if (options.audio_warm_rate <= 0.0) {
1000 audio_warmup_envelope = 1.0F;
1001 return audio_warmup_envelope;
1002 }
1003 if (!audio_warmup_started) {
1004 audio_warmup_started = true;
1006 return audio_warmup_envelope;
1007 }
1008
1009 const float delta = std::max(
1010 std::chrono::duration<float>(now - audio_warmup_last_tick).count(),
1011 0.0F);
1013 audio_warmup_envelope = std::min(
1015 delta * static_cast<float>(options.audio_warm_rate),
1016 1.0F);
1017 return audio_warmup_envelope;
1018 }
1019#endif
1020
1022#ifdef ACMXVK_WITH_DNN
1023 if (!options.human_model.empty()) {
1024 human_segmenter =
1025 std::make_unique<dnn::HumanSegmenter>(options.human_model);
1026 std::cout << "acmxvk: PP-HumanSeg enabled: "
1027 << options.human_model << " ("
1029 ? "background-only shader composition"
1030 : "foreground isolation")
1031 << ", automatic CPU/CUDA backend selection)\n";
1032 }
1033 if (!options.edge_model.empty()) {
1034 edge_detector =
1035 std::make_unique<dnn::EdgeDetector>(options.edge_model);
1036 std::cout << "acmxvk: DexiNed edge detection enabled: "
1038 << " (automatic CPU/CUDA backend selection)\n";
1039 }
1040 if (!options.onnx_configuration.empty()) {
1041 generic_onnx_processor =
1042 std::make_unique<dnn::GenericOnnxProcessor>(
1044 std::cout << "acmxvk: generic ONNX processing enabled: "
1046 << " (automatic CPU/CUDA backend selection)\n";
1047 }
1048#endif
1049 }
1050
1052#ifdef ACMXVK_WITH_DEEP_DREAM
1053 if (options.dream_model.empty()) {
1054 return;
1055 }
1056 deep_dream_model = std::make_unique<dream::Model>(dream::Model::load(
1059 const dream::ModelMetadata &metadata = deep_dream_model->metadata();
1060 const dream::LayerMetadata &layer =
1061 metadata.layers[deep_dream_model->selected_layer()];
1062 if (options.dream_channel >= 0 &&
1063 static_cast<std::size_t>(options.dream_channel) >=
1064 deep_dream_model->selected_channels()) {
1065 throw std::runtime_error(
1066 "--dream-channel is outside the selected layer's channel range");
1067 }
1068 std::cout << "acmxvk: Deep Dream preprocessing enabled: "
1069 << metadata.architecture << '/' << layer.name << ", "
1070 << options.dream_iterations << " iteration(s), strength "
1071 << options.dream_strength << ", feedback "
1072 << options.dream_feedback << ", zoom " << options.dream_zoom
1073 << ", rotation " << options.dream_rotation << " degrees"
1074 << ", working size "
1075 << (options.dream_size == 0
1076 ? std::string("native")
1077 : std::to_string(options.dream_size) + " max")
1078 << ", " << (options.dream_fp16 ? "FP16" : "FP32")
1079 << ", channel ";
1080 if (options.dream_channel < 0) {
1081 std::cout << "all";
1082 } else {
1083 std::cout << options.dream_channel;
1084 }
1085 std::cout << ", " << options.dream_octaves << " octave(s) at "
1087 << "x, jitter " << options.dream_jitter << " px"
1088 << ", smoothing " << options.dream_smoothing << " px"
1089 << ", acidcam-gpu order "
1090 << (options.gpu_filter_before_dream ? "before" : "after")
1091 << " Deep Dream"
1092 << "; output feeds the existing Vulkan shader chain\n";
1094 std::cout << "acmxvk: randomized Deep Dream controls every "
1096 << " media second(s)\n";
1097 }
1099 std::cout << "acmxvk: traditional "
1100 << (options.dream_headless ? "headless" : "preview")
1101 << " Deep Dream video mode: "
1102 "independent source frames, temporal feedback/zoom/"
1103 "rotation disabled, no-drop output\n";
1104 }
1105#endif
1106 }
1107
1109#ifdef ACMXVK_WITH_CUDA
1110 if (options.gpu_filter_indices.empty()) {
1111 return;
1112 }
1113 gpu_filter_engine = std::make_unique<gpu::FilterEngine>(
1115#endif
1116 }
1117
1118 void MainWindow::selectGpuFilter(int direction) {
1119#ifdef ACMXVK_WITH_CUDA
1120 if (gpu_filter_engine != nullptr &&
1121 gpu_filter_engine->select_relative_filter(direction) &&
1124 if (history_initialized) {
1127 }
1128 }
1129#else
1130 static_cast<void>(direction);
1131#endif
1132 }
1133
1135#ifdef MIDI_ENABLED
1137 options.midi_map_file.empty() && midi_cc_mappings.empty()) {
1138 return;
1139 }
1140 midi_input = std::make_unique<midi::MidiInput>();
1141 const int port = options.midi_device_specified ? options.midi_device : 0;
1142 if (!midi_input->open(port)) {
1143 throw std::runtime_error("could not open MIDI input port " +
1144 std::to_string(port));
1145 }
1146#endif
1147 }
1148
1150#ifdef MIDI_ENABLED
1151 if (!options.midi_map_file.empty()) {
1155 std::cout << "acmxvk: loaded " << midi_action_mappings.size()
1156 << " MIDI mapping(s) from " << options.midi_map_file
1157 << '\n';
1158
1159 for (int slider = 0; slider < 4; ++slider) {
1160 const int action = 600 + slider * 2;
1161 const bool mapped = std::any_of(
1162 midi_action_mappings.begin(),
1164 [&](const midi::MidiMapping &mapping) {
1165 return mapping.primary_action == action &&
1166 mapping.secondary_action == action + 1;
1167 });
1168 if (!mapped) {
1169 continue;
1170 }
1171 const std::string name =
1172 "slider" + std::to_string(slider + 1);
1173 const auto uniform = std::find_if(
1174 custom_uniforms.begin(), custom_uniforms.end(),
1175 [&](const ShaderManifest::CustomUniform &candidate) {
1176 return candidate.name == name;
1177 });
1178 if (uniform == custom_uniforms.end()) {
1179 std::cerr << "acmxvk: MIDI " << name
1180 << " mapping has no matching custom uniform in "
1181 "library.json\n";
1182 continue;
1183 }
1184 midi_slider_uniform_indices[slider] = static_cast<int>(
1185 std::distance(custom_uniforms.begin(), uniform));
1186 std::cout << "acmxvk: MIDI Slider " << (slider + 1)
1187 << " -> " << name << " [" << uniform->minimum
1188 << ", " << uniform->maximum << "]\n";
1189 }
1190
1191 std::size_t active_mappings = 0;
1192 for (const midi::MidiMapping &mapping :
1194 if (isMidiMappingSupported(mapping)) {
1195 ++active_mappings;
1196 } else if (options.midi_monitor) {
1197 std::cerr
1198 << "acmxvk: MIDI map action unavailable in this build: "
1199 << mapping.primary_action << ':'
1200 << mapping.secondary_action << '\n';
1201 }
1202 }
1203 std::cout << "acmxvk: MIDI map has " << active_mappings
1204 << " active mapping(s)";
1205 if (active_mappings != midi_action_mappings.size()) {
1206 std::cout << " and "
1207 << (midi_action_mappings.size() - active_mappings)
1208 << " mapping(s) reserved for unported ACMX2 controls";
1209 }
1210 std::cout << '\n';
1211 }
1212
1213 for (const std::string &mapping_text : options.midi_cc_mappings) {
1214 const std::size_t equals = mapping_text.find('=');
1215 if (equals == std::string::npos || equals == 0 ||
1216 equals + 1 >= mapping_text.size() ||
1217 mapping_text.find('=', equals + 1) != std::string::npos) {
1218 throw std::runtime_error(
1219 "--midi-cc requires [channel:]CC=uniform: " +
1220 mapping_text);
1221 }
1222
1223 const std::string source = trim(mapping_text.substr(0, equals));
1224 const std::string uniform_name =
1225 trim(mapping_text.substr(equals + 1));
1226 if (!isValidCustomUniformName(uniform_name)) {
1227 throw std::runtime_error(
1228 "--midi-cc contains an invalid uniform name: " +
1229 uniform_name);
1230 }
1231
1232 int channel = -1;
1233 int controller = 0;
1234 const std::size_t colon = source.find(':');
1235 if (colon == std::string::npos) {
1236 controller = parseInteger(source, "--midi-cc");
1237 } else {
1238 if (colon == 0 || colon + 1 >= source.size() ||
1239 source.find(':', colon + 1) != std::string::npos) {
1240 throw std::runtime_error(
1241 "--midi-cc requires [channel:]CC=uniform: " +
1242 mapping_text);
1243 }
1244 channel = parseInteger(
1245 std::string_view(source).substr(0, colon), "--midi-cc");
1246 controller = parseInteger(
1247 std::string_view(source).substr(colon + 1), "--midi-cc");
1248 if (channel < 1 || channel > 16) {
1249 throw std::runtime_error(
1250 "--midi-cc channel must be between 1 and 16");
1251 }
1252 --channel;
1253 }
1254 if (controller < 0 || controller > 127) {
1255 throw std::runtime_error(
1256 "--midi-cc controller must be between 0 and 127");
1257 }
1258
1259 const auto uniform = std::find_if(
1260 custom_uniforms.begin(), custom_uniforms.end(),
1261 [&](const ShaderManifest::CustomUniform &candidate) {
1262 return candidate.name == uniform_name;
1263 });
1264 if (uniform == custom_uniforms.end()) {
1265 throw std::runtime_error(
1266 "--midi-cc target is not defined in library.json: " +
1267 uniform_name);
1268 }
1269 const std::size_t uniform_index = static_cast<std::size_t>(
1270 std::distance(custom_uniforms.begin(), uniform));
1271 const auto duplicate = std::find_if(
1272 midi_cc_mappings.begin(), midi_cc_mappings.end(),
1273 [&](const MidiCcMapping &mapping) {
1274 return mapping.uniform_index == uniform_index;
1275 });
1276 if (duplicate != midi_cc_mappings.end()) {
1277 throw std::runtime_error(
1278 "custom uniform has more than one --midi-cc mapping: " +
1279 uniform_name);
1280 }
1281
1282 midi_cc_mappings.push_back(
1283 {channel, controller, uniform_index, uniform_name});
1284 std::cout << "acmxvk: MIDI "
1285 << (channel < 0
1286 ? std::string("any channel")
1287 : "channel " + std::to_string(channel + 1))
1288 << " CC " << controller << " -> " << uniform_name
1289 << " [" << uniform->minimum << ", "
1290 << uniform->maximum << "]\n";
1291 }
1292#endif
1293 }
1294
1295#ifdef MIDI_ENABLED
1296 [[nodiscard]] bool MainWindow::applyMidiCc(const midi::MidiMessage &message) {
1297 if (message.bytes.size() < 3 ||
1298 (message.bytes[0] & 0xF0U) != 0xB0U) {
1299 return false;
1300 }
1301 const int channel = message.bytes[0] & 0x0FU;
1302 const int controller = message.bytes[1] & 0x7FU;
1303 const int value = message.bytes[2] & 0x7FU;
1304 bool changed = false;
1305 for (const MidiCcMapping &mapping : midi_cc_mappings) {
1306 if (mapping.controller != controller ||
1307 (mapping.channel >= 0 && mapping.channel != channel)) {
1308 continue;
1309 }
1310 const ShaderManifest::CustomUniform &uniform =
1311 custom_uniforms[mapping.uniform_index];
1312 const double normalized = static_cast<double>(value) / 127.0;
1313 const float mapped = static_cast<float>(
1314 uniform.minimum + normalized *
1315 (uniform.maximum - uniform.minimum));
1316 custom_uniform_values[mapping.uniform_index] = mapped;
1317 changed = true;
1318 if (options.midi_monitor) {
1319 std::cout << "acmxvk: MIDI CC " << controller << " -> "
1320 << mapping.uniform_name << '=' << mapped << '\n';
1321 }
1322 }
1323 return changed;
1324 }
1325
1326 [[nodiscard]] SDL_Keycode MainWindow::midiActionKey(int action) const {
1327 switch (action) {
1328 case 262:
1329#ifdef ACMXVK_WITH_CUDA
1330 return gpu_filter_engine != nullptr ? SDLK_RIGHT : SDLK_UNKNOWN;
1331#else
1332 return SDLK_UNKNOWN;
1333#endif
1334 case 263:
1335#ifdef ACMXVK_WITH_CUDA
1336 return gpu_filter_engine != nullptr ? SDLK_LEFT : SDLK_UNKNOWN;
1337#else
1338 return SDLK_UNKNOWN;
1339#endif
1340 case 264:
1341 return SDLK_DOWN;
1342 case 265:
1343 return SDLK_UP;
1344 case 266:
1345 case 504:
1346 return SDLK_PAGEUP;
1347 case 267:
1348 case 505:
1349 return SDLK_PAGEDOWN;
1350 case 268:
1351#ifdef AUDIO_ENABLED
1352 return SDLK_HOME;
1353#else
1354 return SDLK_UNKNOWN;
1355#endif
1356 case 269:
1357#ifdef AUDIO_ENABLED
1358 return SDLK_END;
1359#else
1360 return SDLK_UNKNOWN;
1361#endif
1362 case 260:
1363 return SDLK_INSERT;
1364 case 261:
1365 return SDLK_DELETE;
1366 case 500:
1367 return SDLK_U;
1368 case 501:
1369 return SDLK_I;
1370 case 298:
1371 return SDLK_F9;
1372 case 32:
1373 return SDLK_SPACE;
1374 case 44:
1375 return options.enable_3d ? SDLK_COMMA : SDLK_UNKNOWN;
1376 case 46:
1377 return options.enable_3d ? SDLK_PERIOD : SDLK_UNKNOWN;
1378 case 51:
1379 return options.enable_3d ? SDLK_3 : SDLK_UNKNOWN;
1380 case 52:
1381 return SDLK_4;
1382 case 53:
1383 return SDLK_5;
1384 case 54:
1385 return SDLK_6;
1386 case 67:
1387 return options.enable_3d ? SDLK_C : SDLK_UNKNOWN;
1388 case 79:
1389 return options.enable_3d ? SDLK_O : SDLK_UNKNOWN;
1390 case 91:
1391 return options.enable_3d ? SDLK_MINUS : SDLK_UNKNOWN;
1392 case 93:
1393 return options.enable_3d ? SDLK_EQUALS : SDLK_UNKNOWN;
1394 case 69:
1395 return options.watermark_text.empty() ? SDLK_UNKNOWN : SDLK_E;
1396 case 74:
1397 return SDLK_J;
1398 case 78:
1399 return SDLK_N;
1400 case 75:
1401 return SDLK_K;
1402 case 76:
1403 return SDLK_L;
1404 case 77:
1405 return SDLK_M;
1406 case 80:
1407 return SDLK_P;
1408 case 81:
1409#ifdef AUDIO_ENABLED
1410 return SDLK_Q;
1411#else
1412 return SDLK_UNKNOWN;
1413#endif
1414 case 70:
1415 return SDLK_F;
1416 case 73:
1417 return SDLK_I;
1418 case 84:
1419 return SDLK_T;
1420 case 85:
1421 return SDLK_U;
1422 case 86:
1423 return options.enable_3d ? SDLK_V : SDLK_UNKNOWN;
1424 case 88:
1425 return options.enable_3d ? SDLK_X : SDLK_UNKNOWN;
1426 case 89:
1427 return SDLK_Y;
1428 case 90:
1429 return SDLK_Z;
1430 default:
1431 return SDLK_UNKNOWN;
1432 }
1433 }
1434
1436 const midi::MidiMapping &mapping) const {
1437 return mapping.primary_action >= 600 &&
1438 mapping.primary_action <= 606 &&
1439 mapping.primary_action % 2 == 0 &&
1440 mapping.secondary_action == mapping.primary_action + 1;
1441 }
1442
1444 const midi::MidiMapping &mapping) {
1445 return mapping.primary_action == 506 ||
1446 mapping.primary_action == 508 ||
1447 mapping.primary_action == 512;
1448 }
1449
1450 [[nodiscard]] bool MainWindow::isMidiModelAction(int action) const {
1451 return options.enable_3d && action >= 506 && action <= 515;
1452 }
1453
1455 const midi::MidiMapping &mapping) const {
1456 if (isMidiSliderMapping(mapping)) {
1457 const int slider = (mapping.primary_action - 600) / 2;
1458 return midi_slider_uniform_indices[slider] >= 0;
1459 }
1460 if (mapping.secondary_action == 0) {
1461 return isMidiModelAction(mapping.primary_action) ||
1462 midiActionKey(mapping.primary_action) != SDLK_UNKNOWN;
1463 }
1464 const bool primary_supported =
1466 midiActionKey(mapping.primary_action) != SDLK_UNKNOWN;
1467 const bool secondary_supported =
1469 midiActionKey(mapping.secondary_action) != SDLK_UNKNOWN;
1470 return primary_supported && secondary_supported;
1471 }
1472
1473 [[nodiscard]] std::string_view MainWindow::midiActionName(int action) const {
1474 switch (action) {
1475 case 262:
1476 return "select next CUDA filter";
1477 case 263:
1478 return "select previous CUDA filter";
1479 case 264:
1480 return "next shader or playlist node";
1481 case 265:
1482 return "previous shader or playlist node";
1483 case 266:
1484 case 504:
1485 return "increase shader time speed";
1486 case 267:
1487 case 505:
1488 return "decrease shader time speed";
1489 case 268:
1490 return "toggle audio delta-time scaling";
1491 case 269:
1492 return "toggle spectrum sensitivity scaling";
1493 case 260:
1494 return "increase audio sensitivity";
1495 case 261:
1496 return "decrease audio sensitivity";
1497 case 500:
1498 return "step shader time forward";
1499 case 501:
1500 return "step shader time backward";
1501 case 506:
1502 return "rotate model X forward";
1503 case 507:
1504 return "rotate model X backward";
1505 case 508:
1506 return "rotate model Y forward";
1507 case 509:
1508 return "rotate model Y backward";
1509 case 510:
1510 return "increase 3D manual rotation speed";
1511 case 511:
1512 return "decrease 3D manual rotation speed";
1513 case 512:
1514 return "rotate model Z forward";
1515 case 513:
1516 return "rotate model Z backward";
1517 case 514:
1518 return "increase model scale";
1519 case 515:
1520 return "decrease model scale";
1521 case 298:
1522 return "toggle runtime HUD";
1523 case 32:
1524 return "toggle shader bypass";
1525 case 44:
1526 return "decrease 3D view rotation speed";
1527 case 46:
1528 return "increase 3D view rotation speed";
1529 case 51:
1530 return "toggle 2D/3D rendering";
1531 case 52:
1532 return "take TIFF snapshot";
1533 case 53:
1534 return "take WebP snapshot";
1535 case 54:
1536 return "take raw RGBA snapshot";
1537 case 67:
1538 return "toggle 3D wave effect";
1539 case 79:
1540 return "toggle 3D scale oscillation";
1541 case 91:
1542 return "decrease model scale";
1543 case 93:
1544 return "increase model scale";
1545 case 69:
1546 return "toggle watermark";
1547 case 74:
1548 return "toggle random autopilot";
1549 case 78:
1550 return "toggle random autopilot crossfade";
1551 case 75:
1552 return "toggle shader lock";
1553 case 76:
1554 return "toggle rendering freeze";
1555 case 77:
1556 return "toggle multipass";
1557 case 80:
1558 return "toggle playlist or input pause";
1559 case 81:
1560 return "toggle audio-reactive shader time";
1561 case 70:
1562 return "toggle fullscreen";
1563 case 73:
1564 return "step shader time backward";
1565 case 84:
1566 return "toggle shader time";
1567 case 85:
1568 return "step shader time forward";
1569 case 86:
1570 return "toggle 3D view rotation";
1571 case 88:
1572 return "reset model view";
1573 case 89:
1574 return "toggle sequential autopilot";
1575 case 90:
1576 return "take screenshot";
1577 default:
1578 return "unsupported action";
1579 }
1580 }
1581
1583 if (!model_initialized || !isMidiModelAction(action)) {
1584 return;
1585 }
1586
1587 const auto rotate = [](float &degrees, float amount) {
1588 degrees = std::fmod(degrees + amount, 360.0F);
1589 if (degrees < 0.0F) {
1590 degrees += 360.0F;
1591 }
1592 };
1593 switch (action) {
1594 case 506:
1597 break;
1598 case 507:
1601 break;
1602 case 508:
1605 break;
1606 case 509:
1609 break;
1610 case 510:
1611 model_camera_rotation_speed = std::clamp(
1612 model_camera_rotation_speed + 0.5F, 0.5F, 50.0F);
1613 std::cout << "acmxvk: 3D manual rotation speed "
1614 << model_camera_rotation_speed << '\n';
1615 break;
1616 case 511:
1617 model_camera_rotation_speed = std::clamp(
1618 model_camera_rotation_speed - 0.5F, 0.5F, 50.0F);
1619 std::cout << "acmxvk: 3D manual rotation speed "
1620 << model_camera_rotation_speed << '\n';
1621 break;
1622 case 512:
1625 break;
1626 case 513:
1629 break;
1630 case 514:
1631 adjustModelScale(0.05F);
1632 break;
1633 case 515:
1634 adjustModelScale(-0.05F);
1635 break;
1636 default:
1637 break;
1638 }
1639 }
1640
1642 if (isMidiModelAction(action)) {
1643 if (options.midi_monitor) {
1644 std::cout << "acmxvk: MIDI action: "
1645 << midiActionName(action) << '\n';
1646 }
1648 return;
1649 }
1650 const SDL_Keycode key = midiActionKey(action);
1651 if (key == SDLK_UNKNOWN) {
1652 return;
1653 }
1654 if (options.midi_monitor) {
1655 std::cout << "acmxvk: MIDI action: " << midiActionName(action)
1656 << '\n';
1657 }
1658 SDL_Event midi_event{};
1659 midi_event.type = SDL_EVENT_KEY_DOWN;
1660 midi_event.key.type = SDL_EVENT_KEY_DOWN;
1661 midi_event.key.key = key;
1662 midi_event.key.mod =
1663 action == 91 || action == 93 ? SDL_KMOD_SHIFT
1664 : SDL_KMOD_NONE;
1665 midi_event.key.repeat = false;
1666 event(midi_event);
1667 }
1668
1669 [[nodiscard]] bool MainWindow::setMidiUniform(std::size_t uniform_index, int value,
1670 std::string_view label) {
1671 if (uniform_index >= custom_uniforms.size() ||
1672 uniform_index >= custom_uniform_values.size()) {
1673 return false;
1674 }
1675 const ShaderManifest::CustomUniform &uniform =
1676 custom_uniforms[uniform_index];
1677 const double normalized = static_cast<double>(value) / 127.0;
1678 const float mapped = static_cast<float>(
1679 uniform.minimum +
1680 normalized * (uniform.maximum - uniform.minimum));
1681 custom_uniform_values[uniform_index] = mapped;
1682 if (options.midi_monitor) {
1683 std::cout << "acmxvk: MIDI " << label << " -> "
1684 << uniform.name << '=' << mapped << '\n';
1685 }
1686 return true;
1687 }
1688
1689 [[nodiscard]] bool MainWindow::applyMidiMap(const midi::MidiMessage &message) {
1690 if (message.bytes.size() < 3) {
1691 return false;
1692 }
1693 bool changed = false;
1694 for (std::size_t index = 0; index < midi_action_mappings.size();
1695 ++index) {
1696 const midi::MidiMapping &mapping = midi_action_mappings[index];
1697 if (message.bytes[0] != mapping.status ||
1698 message.bytes[1] != mapping.data1) {
1699 continue;
1700 }
1701 const int value = message.bytes[2] & 0x7FU;
1702 if (mapping.secondary_action == 0) {
1703 if (message.bytes[2] == mapping.data2) {
1705 }
1706 continue;
1707 }
1708
1709 if (isMidiSliderMapping(mapping)) {
1710 const int slider = (mapping.primary_action - 600) / 2;
1711 const int uniform_index =
1713 if (uniform_index >= 0) {
1714 changed =
1716 static_cast<std::size_t>(uniform_index), value,
1717 "Slider " + std::to_string(slider + 1)) ||
1718 changed;
1719 }
1720 continue;
1721 }
1722
1723 MidiKnobState &state = midi_knob_states[index];
1724 if (usesMidiDeltaDirection(mapping) &&
1725 value != state.previous_value) {
1726 state.direction_action =
1727 value > state.previous_value
1728 ? mapping.primary_action
1729 : mapping.secondary_action;
1730 }
1731 state.previous_value = value;
1732 state.value = value;
1733 state.active = value != 64;
1734 if (!state.active) {
1735 state.frame_counter = 0;
1736 }
1737 }
1738 return changed;
1739 }
1740
1742 for (std::size_t index = 0; index < midi_action_mappings.size();
1743 ++index) {
1744 const midi::MidiMapping &mapping = midi_action_mappings[index];
1745 MidiKnobState &state = midi_knob_states[index];
1746 if (!state.active || mapping.secondary_action == 0 ||
1747 isMidiSliderMapping(mapping) ||
1748 !isMidiMappingSupported(mapping)) {
1749 continue;
1750 }
1751
1752 const int distance = std::abs(state.value - 64);
1753 const int frame_skip =
1754 std::max(1, 17 - (distance * 16 / 63));
1755 if (++state.frame_counter < frame_skip) {
1756 continue;
1757 }
1758 state.frame_counter = 0;
1759 int action = state.value > 64
1760 ? mapping.primary_action
1761 : mapping.secondary_action;
1762 if (usesMidiDeltaDirection(mapping)) {
1763 action = state.direction_action;
1764 if (action == 0) {
1765 continue;
1766 }
1767 }
1768 dispatchMidiAction(action);
1769 }
1770 }
1771#endif
1772
1774 if (frame_sprite != nullptr) {
1775 frame_sprite->setCustomUniforms(custom_uniform_values);
1776 }
1777 for (mxvk::VK_Sprite *sprite : post_process_sprites) {
1778 sprite->setCustomUniforms(custom_uniform_values);
1779 }
1780 }
1781
1783#ifdef MIDI_ENABLED
1784 if (midi_input == nullptr || !midi_input->is_open()) {
1785 return;
1786 }
1787 const std::vector<midi::MidiMessage> messages =
1788 midi_input->poll_messages();
1789 bool custom_uniforms_changed = false;
1790 for (const midi::MidiMessage &message : messages) {
1791 custom_uniforms_changed =
1792 applyMidiCc(message) || custom_uniforms_changed;
1793 custom_uniforms_changed =
1794 applyMidiMap(message) || custom_uniforms_changed;
1795 }
1797 if (custom_uniforms_changed) {
1799 }
1800 if (options.midi_monitor) {
1801 for (const midi::MidiMessage &message : messages) {
1802 std::ostringstream text;
1803 text << "acmxvk: MIDI #" << message.sequence << " +"
1804 << std::fixed << std::setprecision(6)
1805 << message.delta_seconds << "s [";
1806 for (std::size_t index = 0; index < message.bytes.size();
1807 ++index) {
1808 if (index > 0) {
1809 text << ' ';
1810 }
1811 text << std::hex << std::uppercase << std::setfill('0')
1812 << std::setw(2)
1813 << static_cast<unsigned int>(message.bytes[index]);
1814 }
1815 text << ']';
1816 std::cout << text.str() << '\n';
1817 }
1818 }
1819 const std::uint64_t dropped = midi_input->dropped_message_count();
1820 if (dropped != observed_midi_drops) {
1821 std::cerr << "acmxvk: MIDI queue dropped " << dropped
1822 << " message(s) total\n";
1823 observed_midi_drops = dropped;
1824 }
1825#endif
1826 }
1827
1829 if (!options.enable_audio) {
1830 return;
1831 }
1832#ifdef AUDIO_ENABLED
1833 audio_engine = std::make_unique<audio::AudioEngine>();
1834 audio_engine->set_sensitivity(
1835 static_cast<float>(options.audio_sensitivity));
1836 if (!options.audio_file.empty()) {
1837 file_audio_source = std::make_unique<audio::FileAudioSource>();
1838 if (!file_audio_source->open(options.audio_file)) {
1840 std::cerr
1841 << "acmxvk: source video has no decodable audio "
1842 "track; continuing with silent audio-reactive "
1843 "values";
1845 std::cerr << " and pass-through disabled";
1846 }
1847 std::cerr << '\n';
1848 file_audio_source.reset();
1849 return;
1850 }
1851 throw std::runtime_error("could not decode --audio-file: " +
1853 }
1855 std::cout << "acmxvk: source video audio drives shader "
1856 "reactivity\n";
1857 }
1860 !file_audio_source->enable_output(
1862 static_cast<float>(options.audio_pass_through_gain))) {
1863 std::cerr << "acmxvk: file audio output could not be "
1864 "initialized; continuing with silent analysis\n";
1865 }
1867 return;
1868 }
1869 const audio::AudioStreamConfig config{
1870 static_cast<unsigned int>(options.audio_channels),
1871 static_cast<float>(options.audio_sensitivity),
1875 static_cast<float>(options.audio_pass_through_gain),
1876 static_cast<float>(options.audio_recording_gain),
1877 };
1878 if (!audio_engine->open(config)) {
1879 std::cerr << "acmxvk: audio input could not be initialized; "
1880 "continuing with zero-valued audio metrics\n";
1881 audio_engine.reset();
1882 } else {
1884 }
1885#endif
1886 }
1887
1889 if (options.record_audio_file.empty()) {
1890 return;
1891 }
1892#ifdef AUDIO_ENABLED
1893 if (audio_engine == nullptr || file_audio_source != nullptr ||
1894 !audio_engine->is_open()) {
1895 throw std::runtime_error(
1896 "--record-audio requires an active live audio input");
1897 }
1898 if (!options.output_file.empty() && !options.png_output) {
1899 return;
1900 }
1901 if (!audio_engine->is_recording() &&
1902 !audio_engine->start_recording()) {
1903 throw std::runtime_error(
1904 "could not start standalone microphone recording");
1905 }
1906#endif
1907 }
1908
1910#ifdef AUDIO_ENABLED
1911 if (audioSourceOpen()) {
1912 audio_engine->set_sensitivity(audio_engine->sensitivity() + amount);
1913 options.audio_sensitivity = audio_engine->sensitivity();
1914 std::cout << "acmxvk: audio sensitivity "
1915 << options.audio_sensitivity << '\n';
1916 return;
1917 }
1918#else
1919 static_cast<void>(amount);
1920#endif
1921 std::cout << "acmxvk: audio input is not active\n";
1922 }
1923
1924 [[nodiscard]] bool MainWindow::audioSourceOpen() const {
1925#ifdef AUDIO_ENABLED
1926 return audio_engine != nullptr &&
1927 (audio_engine->is_open() ||
1928 (file_audio_source != nullptr && file_audio_source->is_open()));
1929#else
1930 return false;
1931#endif
1932 }
1933
1935#ifdef AUDIO_ENABLED
1936 if (audio_engine == nullptr || file_audio_source != nullptr ||
1937 !audio_engine->is_open() || audio_engine->is_recording() ||
1938 !writer.is_open() || options.png_output ||
1940 options.record_audio_file.empty())) {
1941 return;
1942 }
1943 if (!audio_engine->start_recording()) {
1944 std::cerr << "acmxvk: could not start live audio recording; "
1945 "continuing with video-only output\n";
1946 }
1947#endif
1948 }
1949
1952 return;
1953 }
1955 hud_session_start = std::chrono::steady_clock::now();
1962#ifdef AUDIO_ENABLED
1964#endif
1966 std::cout << "acmxvk: media timeline started on first source frame\n";
1967 }
1968
1972 paused == source_playback_clock_paused) {
1973 return;
1974 }
1975
1976 const auto now = std::chrono::steady_clock::now();
1977 if (paused) {
1979 } else {
1982 }
1984 }
1985
1986 [[nodiscard]] bool MainWindow::mediaClockSeconds(double &seconds) const {
1987#ifdef AUDIO_ENABLED
1988 if (file_audio_source != nullptr &&
1989 file_audio_source->has_output_clock()) {
1990 seconds = file_audio_source->playback_time();
1991 return true;
1992 }
1993 if ((!options.copy_audio || options.mute_output) && writer.is_open() &&
1994 audio_engine != nullptr && file_audio_source == nullptr &&
1995 audio_engine->is_recording()) {
1996 seconds = audio_engine->recording_time();
1997 return true;
1998 }
1999#endif
2002 seconds = hudWallElapsedSeconds();
2003 return true;
2004 }
2007 const auto clock_end = source_playback_clock_paused
2009 : std::chrono::steady_clock::now();
2010 const auto active_time =
2011 clock_end - source_playback_clock_start -
2013 seconds = std::max(
2014 0.0, std::chrono::duration<double>(active_time).count());
2015 return true;
2016 }
2017 seconds = 0.0;
2018 return false;
2019 }
2020 // Shader discovery, custom uniforms, interface IPC, and playlists.
2022 if (!options.fragment_shader.empty() ||
2023 !options.compute_shader.empty()) {
2024 const bool compute = !options.compute_shader.empty();
2025 const fs::path shader = fs::absolute(
2027 .lexically_normal();
2028 const std::string label =
2029 compute ? "compute shader" : "fragment shader";
2030 if (shader.extension() != ".spv" ||
2031 !fs::is_regular_file(shader)) {
2032 throw std::runtime_error(
2033 label + " is not a readable .spv file: " +
2034 shader.string());
2035 }
2036 input::validate_spirv_file(shader, label);
2037 const mxvk::ShaderModuleInfo module_info =
2038 mxvk::inspect_spirv(mxvk::load_spv(shader.string()));
2039 const mxvk::ShaderStage expected_stage =
2040 compute ? mxvk::ShaderStage::Compute
2041 : mxvk::ShaderStage::Fragment;
2042 if (module_info.stage != expected_stage) {
2043 throw std::runtime_error(
2044 label + " SPIR-V entry point has the wrong shader stage: " +
2045 shader.string());
2046 }
2047 recordShaderResources(module_info, "shader");
2048 shaders.push_back(shader);
2049 return;
2050 }
2051 if (options.shader_directory.empty()) {
2052 return;
2053 }
2054
2056 fs::absolute(options.shader_directory).lexically_normal();
2057 const ShaderManifest manifest =
2059 shader_manifest_path = manifest.path;
2062 for (const std::string &entry : manifest.entries) {
2063 const fs::path shader =
2065 if (!shader.empty()) {
2067 "shader manifest entry");
2068 const mxvk::ShaderModuleInfo module_info =
2069 mxvk::inspect_spirv(mxvk::load_spv(shader.string()));
2070 recordShaderResources(module_info, "shader library");
2071 shaders.push_back(shader);
2072 }
2073 }
2074 std::sort(shaders.begin(), shaders.end(), [](const fs::path &left, const fs::path &right) {
2075 std::string left_text = left.generic_string();
2076 std::string right_text = right.generic_string();
2077 std::transform(left_text.begin(), left_text.end(), left_text.begin(),
2078 [](unsigned char character) {
2079 return static_cast<char>(std::tolower(character));
2080 });
2081 std::transform(right_text.begin(), right_text.end(), right_text.begin(),
2082 [](unsigned char character) {
2083 return static_cast<char>(std::tolower(character));
2084 });
2085 return left_text < right_text;
2086 });
2087 if (shaders.empty()) {
2088 throw std::runtime_error("shader manifest contains no readable SPIR-V files: " +
2089 shader_manifest_path.string());
2090 }
2091 std::cout << "acmxvk: loaded " << shaders.size() << " shaders from "
2092 << shader_manifest_path.string() << '\n';
2093 printCustomUniforms();
2094
2095 if (!options.shader_file.empty()) {
2096 const auto selected = std::find_if(
2097 shaders.begin(), shaders.end(), [&](const fs::path &path) {
2098 fs::path requested(options.shader_file);
2099 if (requested.extension() != ".spv") {
2100 requested.replace_extension(".spv");
2101 }
2102 return path.filename() == requested.filename() ||
2103 path.lexically_relative(shader_library_directory) == requested;
2104 });
2105 if (selected == shaders.end()) {
2106 throw std::runtime_error("shader file is not listed in the manifest: " +
2107 options.shader_file);
2108 }
2109 shader_index = static_cast<std::size_t>(std::distance(shaders.begin(), selected));
2110 } else {
2111 const int count = static_cast<int>(shaders.size());
2112 const int wrapped_index = ((options.shader_index % count) + count) % count;
2113 shader_index = static_cast<std::size_t>(wrapped_index);
2114 }
2115 }
2116
2118 for (const std::string &override_text :
2119 options.custom_uniform_overrides) {
2120 const std::size_t separator = override_text.find('=');
2121 if (separator == std::string::npos || separator == 0 ||
2122 separator + 1 >= override_text.size()) {
2123 throw std::runtime_error(
2124 "--uniform requires name=value: " + override_text);
2125 }
2126 const std::string name = trim(override_text.substr(0, separator));
2127 const double value = parseNumber(
2128 trim(override_text.substr(separator + 1)), "--uniform");
2129 const auto match = std::find_if(
2130 custom_uniforms.begin(), custom_uniforms.end(),
2131 [&](const ShaderManifest::CustomUniform &uniform) {
2132 return uniform.name == name;
2133 });
2134 if (match == custom_uniforms.end()) {
2135 throw std::runtime_error(
2136 "custom uniform is not defined in library.json: " + name);
2137 }
2138 match->value = std::clamp(value, match->minimum, match->maximum);
2139 }
2140
2141 custom_uniform_values.clear();
2142 custom_uniform_values.reserve(custom_uniforms.size());
2143 for (const ShaderManifest::CustomUniform &uniform : custom_uniforms) {
2144 custom_uniform_values.push_back(static_cast<float>(uniform.value));
2145 }
2146 }
2147
2149 if (custom_uniforms.empty()) {
2150 return;
2151 }
2152 constexpr std::string_view COMPONENTS = "xyzw";
2153 std::cout << "acmxvk: custom uniforms (binding 1):\n";
2154 for (std::size_t index = 0; index < custom_uniforms.size(); ++index) {
2155 const ShaderManifest::CustomUniform &uniform = custom_uniforms[index];
2156 std::cout << " " << uniform.name << '=' << uniform.value
2157 << " -> custom_uniforms[" << (index / 4) << "]."
2158 << COMPONENTS[index % 4] << '\n';
2159 }
2160 }
2161
2162 [[nodiscard]] std::string MainWindow::currentShader() const {
2163 return shaders.empty() ? std::string{} : shaders[shader_index].string();
2164 }
2165
2166 [[nodiscard]] fs::path
2167 MainWindow::resolvedShaderPath(const fs::path &shader) const {
2168 std::error_code error;
2169 const fs::path canonical = fs::weakly_canonical(shader, error);
2170 const std::string key =
2171 (error ? shader.lexically_normal() : canonical).string();
2172 const auto override = shader_reload_overrides.find(key);
2173 return override == shader_reload_overrides.end() ? shader
2174 : override->second;
2175 }
2176
2177 [[nodiscard]] bool MainWindow::historyCacheEnabled() const {
2178 return options.enable_texture_cache || shader_history_required;
2179 }
2180
2181 void MainWindow::recordShaderResources(const mxvk::ShaderModuleInfo &module_info,
2182 std::string_view source) {
2183 if (module_info.usesHistoryTexture &&
2186 std::cout << "acmxvk: enabled shared history for " << source
2187 << " binding 2\n";
2188 }
2189 if (module_info.usesSpectrumTexture &&
2192 std::cout << "acmxvk: enabled spectrum descriptor for " << source
2193 << " binding 3\n";
2194 }
2195 if (module_info.usesSpectrumHistoryTexture &&
2198 if (options.audio_buffers == 0) {
2199 options.audio_buffers = 8;
2200 }
2201 std::cout << "acmxvk: enabled " << options.audio_buffers
2202 << " spectrum-history layers for " << source
2203 << " binding 4\n";
2204 }
2205 }
2206
2207 [[nodiscard]] std::uint32_t MainWindow::spectrumBinCount() const {
2208#ifdef AUDIO_ENABLED
2210#else
2212#endif
2213 }
2214
2216#ifdef AUDIO_ENABLED
2217 return true;
2218#else
2220#endif
2221 }
2222
2224 return options.audio_buffers > 0;
2225 }
2226
2228 if (!options.interface_shm) {
2229 return;
2230 }
2231
2232 const auto now = std::chrono::steady_clock::now();
2234 return;
2235 }
2236 interface_next_connect_attempt = now + std::chrono::seconds(2);
2237 if (!interface_client.open()) {
2238 return;
2239 }
2240
2241 InterfaceState state;
2242 if (!interface_client.read(state)) {
2244 std::cerr << "acmxvk: could not read compatible interface "
2245 "control state; retrying\n";
2247 }
2248 interface_client.close();
2249 return;
2250 }
2251 const bool reconnected = interface_connection_warning_reported;
2264 std::cout << "acmxvk: interface live shader, multipass, playback, "
2265 "overlay, GPU-filter, Deep Dream, and audio-file control "
2266 "enabled"
2267 << (reconnected ? " (reconnected)" : "") << '\n';
2268 }
2269
2271 if (!options.interface_shm) {
2272 return;
2273 }
2274 if (!interface_client.is_open()) {
2276 return;
2277 }
2278
2279 InterfaceState state;
2280 if (!interface_client.read(state)) {
2282 std::cerr << "acmxvk: interface control connection lost; "
2283 "retrying\n";
2285 }
2286 interface_client.close();
2288 std::chrono::steady_clock::now() + std::chrono::seconds(2);
2289 return;
2290 }
2291 if (state.sequence == interface_last_sequence) {
2292 return;
2293 }
2302 if (state.audio_file.request_sequence !=
2307 }
2308 if (state.reload.request_sequence !=
2312 }
2313 }
2314
2316 const InterfacePlaybackState &requested, bool announce) {
2317 if (options.repeat != requested.repeat) {
2318 options.repeat = requested.repeat;
2319 if (announce) {
2320 std::cout << "acmxvk: interface video repeat "
2321 << (options.repeat ? "enabled" : "disabled")
2322 << '\n';
2323 }
2324 }
2325 if (options.normalized_time != requested.normalized_time) {
2326 options.normalized_time = requested.normalized_time;
2327 if (announce) {
2328 std::cout << "acmxvk: interface normalized time "
2329 << (options.normalized_time ? "enabled"
2330 : "disabled")
2331 << '\n';
2332 }
2333 }
2334 }
2335
2337 bool announce) {
2338 if (options.display_filter != requested.display_filter) {
2339 options.display_filter = requested.display_filter;
2340 if (announce) {
2341 std::cout << "acmxvk: interface display-filter overlay "
2342 << (options.display_filter ? "enabled"
2343 : "disabled")
2344 << '\n';
2345 }
2346 }
2347
2348 try {
2351 "interface watermark", true);
2352 } catch (const std::exception &error) {
2353 std::cerr << "acmxvk: rejected interface watermark: "
2354 << error.what() << '\n';
2355 return;
2356 }
2357
2358 const bool was_enabled =
2359 watermark_enabled && !options.watermark_text.empty();
2360 const bool requested_enabled =
2361 requested.watermark_enabled &&
2362 !requested.watermark_text.empty();
2363 const bool changed =
2364 watermark_enabled != requested_enabled ||
2365 options.watermark_text != requested.watermark_text ||
2366 options.watermark_color != requested.watermark_color;
2367 if (!changed) {
2368 return;
2369 }
2370
2371 options.watermark_text = requested.watermark_text;
2372 options.watermark_color = requested.watermark_color;
2373 watermark_enabled = requested_enabled;
2374 if (!was_enabled && watermark_enabled) {
2375 counter_disabled = true;
2376 }
2377 if (announce) {
2378 std::cout << "acmxvk: interface watermark "
2379 << (watermark_enabled ? "enabled" : "disabled");
2380 if (watermark_enabled) {
2381 std::cout << " (color="
2382 << static_cast<int>(options.watermark_color[0])
2383 << ','
2384 << static_cast<int>(options.watermark_color[1])
2385 << ','
2386 << static_cast<int>(options.watermark_color[2])
2387 << ')';
2388 }
2389 std::cout << '\n';
2390 }
2391 }
2392
2394 const InterfaceGpuFilterState &requested, bool announce) {
2395#ifdef ACMXVK_WITH_CUDA
2396 const bool requested_enabled =
2397 requested.enabled && !requested.filter_indices.empty();
2398 const bool currently_enabled = gpu_filter_engine != nullptr;
2399 const std::vector<int> effective_indices =
2400 requested_enabled ? requested.filter_indices
2401 : std::vector<int>{};
2402 if (requested_enabled == currently_enabled &&
2403 options.gpu_filter_indices == effective_indices &&
2404 (!requested_enabled ||
2405 options.gpu_frame_buffer_size ==
2406 requested.frame_buffer_size)) {
2407 return;
2408 }
2409
2410 if (requested.enabled && requested.filter_indices.empty()) {
2411 std::cerr << "acmxvk: rejected enabled interface GPU-filter "
2412 "state without any filter indices\n";
2413 return;
2414 }
2415
2416 std::unique_ptr<gpu::FilterEngine> replacement;
2417 if (requested_enabled) {
2418 try {
2419 replacement = std::make_unique<gpu::FilterEngine>(
2420 requested.filter_indices,
2421 requested.frame_buffer_size);
2422 } catch (const std::exception &error) {
2423 std::cerr
2424 << "acmxvk: rejected interface GPU-filter state: "
2425 << error.what() << '\n';
2426 return;
2427 }
2428 }
2429
2430 gpu_filter_engine = std::move(replacement);
2431 options.gpu_filter_indices = effective_indices;
2432 if (requested_enabled) {
2433 options.gpu_frame_buffer_size = requested.frame_buffer_size;
2434 }
2435
2436 if (frame_sprite != nullptr &&
2439 if (history_initialized) {
2442 }
2443 }
2444
2445 if (announce) {
2446 std::cout << "acmxvk: interface CUDA filter chain "
2447 << (requested_enabled ? "enabled" : "disabled");
2448 if (requested_enabled) {
2449 std::cout << " (" << requested.filter_indices.size()
2450 << " filters, " << requested.frame_buffer_size
2451 << " history frames)";
2452 }
2453 std::cout << '\n';
2454 }
2455#else
2456 if (announce && requested.enabled) {
2457 std::cerr << "acmxvk: ignored interface GPU-filter state: this "
2458 "build does not include acidcam-gpu\n";
2459 }
2460#endif
2461 }
2462
2464 const InterfaceDeepDreamState &requested, bool announce) {
2465#ifdef ACMXVK_WITH_DEEP_DREAM
2466 const bool currently_enabled = deep_dream_model != nullptr;
2467 if (!requested.enabled) {
2468 if (!currently_enabled) {
2469 return;
2470 }
2471 deep_dream_model.reset();
2472#ifdef ACMXVK_WITH_MXVK_CUDA
2473 cuda_dream_rgba.release();
2474#endif
2475 options.dream_model.clear();
2476 options.dream_layer.clear();
2477 options.gpu_filter_before_dream = false;
2479 if (announce) {
2480 std::cout << "acmxvk: interface Deep Dream disabled\n";
2481 }
2482 return;
2483 }
2484
2485 try {
2488 "interface Deep Dream model path");
2489 input::validate_string(requested.layer,
2491 "interface Deep Dream layer");
2492 if (requested.iterations < 1 || requested.iterations > 100) {
2493 throw std::runtime_error(
2494 "iterations must be between 1 and 100");
2495 }
2496 if (!std::isfinite(requested.strength) ||
2497 requested.strength <= 0.0F || requested.strength > 10.0F) {
2498 throw std::runtime_error(
2499 "strength must be greater than 0 and no more than 10");
2500 }
2501 if (!std::isfinite(requested.feedback) ||
2502 requested.feedback < 0.0F || requested.feedback > 0.99F) {
2503 throw std::runtime_error(
2504 "feedback must be between 0 and 0.99");
2505 }
2506 if (!std::isfinite(requested.zoom) || requested.zoom < 0.9F ||
2507 requested.zoom > 1.1F) {
2508 throw std::runtime_error("zoom must be between 0.9 and 1.1");
2509 }
2510 if (!std::isfinite(requested.rotation) ||
2511 requested.rotation < -5.0F || requested.rotation > 5.0F) {
2512 throw std::runtime_error(
2513 "rotation must be between -5 and 5 degrees");
2514 }
2515 if (requested.maximum_dimension != 0 &&
2516 (requested.maximum_dimension < 64 ||
2517 requested.maximum_dimension > 4096)) {
2518 throw std::runtime_error(
2519 "maximum dimension must be 0 or between 64 and 4096");
2520 }
2521 if (requested.channel < -1 || requested.channel > 65535) {
2522 throw std::runtime_error(
2523 "channel must be all channels or between 0 and 65535");
2524 }
2525 if (requested.octaves < 1 || requested.octaves > 8) {
2526 throw std::runtime_error("octaves must be between 1 and 8");
2527 }
2528 if (!std::isfinite(requested.octave_scale) ||
2529 requested.octave_scale < 1.1F ||
2530 requested.octave_scale > 3.0F) {
2531 throw std::runtime_error(
2532 "octave scale must be between 1.1 and 3.0");
2533 }
2534 if (requested.jitter < 0 || requested.jitter > 64) {
2535 throw std::runtime_error("jitter must be between 0 and 64");
2536 }
2537 if (requested.smoothing < 0 || requested.smoothing > 16) {
2538 throw std::runtime_error(
2539 "smoothing must be between 0 and 16");
2540 }
2541 if (requested.gpu_filter_first) {
2542#ifdef ACMXVK_WITH_CUDA
2543 if (gpu_filter_engine == nullptr) {
2544 throw std::runtime_error(
2545 "acidcam-gpu-first requires an enabled GPU filter chain");
2546 }
2547#else
2548 throw std::runtime_error(
2549 "acidcam-gpu-first requires acidcam-gpu support");
2550#endif
2551 if (options.maximize_fps) {
2552 throw std::runtime_error(
2553 "acidcam-gpu-first cannot be used with maximize FPS");
2554 }
2556 throw std::runtime_error(
2557 "acidcam-gpu-first supports camera and video input");
2558 }
2559 if (!options.edge_model.empty() ||
2560 !options.human_model.empty() ||
2561 !options.onnx_configuration.empty()) {
2562 throw std::runtime_error(
2563 "acidcam-gpu-first cannot be combined with DNN input effects");
2564 }
2566 throw std::runtime_error(
2567 "acidcam-gpu-first cannot be enabled for HDR input");
2568 }
2569 }
2570
2571 const bool settings_changed =
2572 !currently_enabled ||
2573 options.dream_model != requested.model_path ||
2574 options.dream_layer != requested.layer ||
2575 options.dream_fp16 != requested.fp16 ||
2576 options.dream_iterations != requested.iterations ||
2577 options.dream_size != requested.maximum_dimension ||
2578 options.dream_channel != requested.channel ||
2579 options.dream_octaves != requested.octaves ||
2580 options.dream_jitter != requested.jitter ||
2581 options.dream_smoothing != requested.smoothing ||
2582 options.dream_strength != requested.strength ||
2583 options.dream_feedback != requested.feedback ||
2584 options.dream_zoom != requested.zoom ||
2585 options.dream_rotation != requested.rotation ||
2586 options.dream_octave_scale != requested.octave_scale ||
2587 options.gpu_filter_before_dream !=
2588 requested.gpu_filter_first;
2589 if (!settings_changed) {
2590 return;
2591 }
2592
2593 const bool reload_model =
2594 !currently_enabled ||
2595 options.dream_model != requested.model_path ||
2596 options.dream_layer != requested.layer ||
2597 options.dream_fp16 != requested.fp16;
2598 std::unique_ptr<dream::Model> replacement;
2599 dream::Model *validated_model = deep_dream_model.get();
2600 if (reload_model) {
2601 replacement = std::make_unique<dream::Model>(
2603 options.cuda_device, requested.layer,
2604 requested.fp16));
2605 validated_model = replacement.get();
2606 }
2607 if (requested.channel >= 0 &&
2608 static_cast<std::size_t>(requested.channel) >=
2609 validated_model->selected_channels()) {
2610 throw std::runtime_error(
2611 "channel is outside the selected layer's channel range");
2612 }
2613
2614 if (replacement != nullptr) {
2615 deep_dream_model = std::move(replacement);
2616#ifdef ACMXVK_WITH_MXVK_CUDA
2617 cuda_dream_rgba.release();
2618#endif
2619 }
2620 options.dream_model = requested.model_path;
2621 options.dream_layer = requested.layer;
2622 options.dream_fp16 = requested.fp16;
2623 options.dream_iterations = requested.iterations;
2624 options.dream_size = requested.maximum_dimension;
2625 options.dream_channel = requested.channel;
2626 options.dream_octaves = requested.octaves;
2627 options.dream_jitter = requested.jitter;
2628 options.dream_smoothing = requested.smoothing;
2629 options.dream_strength = requested.strength;
2630 options.dream_feedback = requested.feedback;
2631 options.dream_zoom = requested.zoom;
2632 options.dream_rotation = requested.rotation;
2633 options.dream_octave_scale = requested.octave_scale;
2634 options.gpu_filter_before_dream = requested.gpu_filter_first;
2636
2637 if (announce) {
2638 std::cout << "acmxvk: interface Deep Dream settings applied: "
2639 << requested.layer << ", "
2640 << requested.iterations << " iteration(s), strength "
2641 << requested.strength << ", feedback "
2642 << requested.feedback << ", zoom " << requested.zoom
2643 << ", rotation " << requested.rotation
2644 << " degrees, "
2645 << (requested.gpu_filter_first
2646 ? "acidcam-gpu first"
2647 : "Deep Dream first")
2648 << (reload_model ? " (model reloaded)" : "")
2649 << '\n';
2650 }
2651 } catch (const std::exception &error) {
2652 std::cerr << "acmxvk: rejected interface Deep Dream settings: "
2653 << error.what() << '\n';
2654 }
2655#else
2656 if (announce && requested.enabled) {
2657 std::cerr << "acmxvk: ignored interface Deep Dream settings: this "
2658 "build does not include Deep Dream support\n";
2659 }
2660#endif
2661 }
2662
2664 const InterfaceAudioFileState &requested) {
2665#ifdef AUDIO_ENABLED
2666 if (file_audio_source == nullptr || audio_engine == nullptr) {
2667 std::cerr
2668 << "acmxvk: ignored live audio-file change because this "
2669 "process was not started in audio-file mode\n";
2670 return;
2671 }
2672 if (requested.path.empty()) {
2673 std::cerr
2674 << "acmxvk: rejected empty interface audio-file request\n";
2675 return;
2676 }
2677
2678 auto replacement = std::make_unique<audio::FileAudioSource>();
2679 try {
2680 if (!replacement->open(requested.path)) {
2681 std::cerr << "acmxvk: could not switch file audio to: "
2682 << requested.path << '\n';
2683 return;
2684 }
2685 } catch (const std::exception &error) {
2686 std::cerr << "acmxvk: rejected interface audio-file request: "
2687 << error.what() << '\n';
2688 return;
2689 }
2690
2691 replacement->set_repeat(requested.repeat);
2692 if (requested.pass_through &&
2693 !replacement->enable_output(
2694 requested.output_device,
2695 static_cast<float>(options.audio_pass_through_gain))) {
2696 std::cerr
2697 << "acmxvk: live audio-file output could not be opened; "
2698 "continuing with visual reactivity only\n";
2699 }
2700
2701 file_audio_source->stop_output();
2702 file_audio_source = std::move(replacement);
2703 options.audio_file = requested.path;
2704 options.audio_output_device = requested.output_device;
2705 options.audio_pass_through = requested.pass_through;
2706 options.audio_trunc = requested.trunc;
2707 options.audio_repeat = requested.repeat;
2708 audio_engine->reset();
2710 std::cout << "acmxvk: switched file audio to: "
2711 << file_audio_source->path() << " (repeat="
2712 << (options.audio_repeat ? "on" : "off")
2713 << ", trunc=" << (options.audio_trunc ? "on" : "off")
2714 << ", pass-through="
2715 << (options.audio_pass_through ? "on" : "off")
2716 << ")\n";
2717#else
2718 static_cast<void>(requested);
2719 std::cerr << "acmxvk: ignored interface audio-file request: this "
2720 "build does not include audio support\n";
2721#endif
2722 }
2723
2725 const InterfaceReloadState &requested) {
2726 if (requested.path.empty()) {
2727 std::cerr
2728 << "acmxvk: rejected empty interface shader reload\n";
2729 return;
2730 }
2731
2732 try {
2733 input::validate_string(requested.path,
2735 "interface shader reload path");
2736 } catch (const std::exception &error) {
2737 std::cerr << "acmxvk: rejected interface shader reload: "
2738 << error.what() << '\n';
2739 return;
2740 }
2741
2742 std::error_code error;
2743 const fs::path requested_path =
2744 fs::weakly_canonical(requested.path, error);
2745 if (error || requested_path.empty() ||
2746 !fs::is_regular_file(requested_path)) {
2747 std::cerr << "acmxvk: interface shader reload file is not "
2748 "readable: "
2749 << requested.path << '\n';
2750 return;
2751 }
2752
2753 std::size_t logical_index = shaders.size();
2754 const auto shader_match = std::find_if(
2755 shaders.begin(), shaders.end(),
2756 [&](const fs::path &shader) {
2757 std::error_code shader_error;
2758 const fs::path canonical_shader =
2759 fs::weakly_canonical(shader, shader_error);
2760 return !shader_error && canonical_shader == requested_path;
2761 });
2762 if (shader_match != shaders.end()) {
2763 logical_index = static_cast<std::size_t>(
2764 std::distance(shaders.begin(), shader_match));
2765 } else if (requested.shader_index >= 0 &&
2766 static_cast<std::size_t>(requested.shader_index) <
2767 shaders.size()) {
2768 logical_index = static_cast<std::size_t>(requested.shader_index);
2769 }
2770 if (logical_index >= shaders.size()) {
2771 std::cerr << "acmxvk: interface shader reload does not identify "
2772 "a shader in the active runtime library: "
2773 << requested_path.string() << '\n';
2774 return;
2775 }
2776
2777 const fs::path logical_shader = shaders[logical_index];
2778 std::error_code logical_error;
2779 const fs::path canonical_logical =
2780 fs::weakly_canonical(logical_shader, logical_error);
2781 if (logical_error || canonical_logical.empty()) {
2782 std::cerr << "acmxvk: interface shader reload could not resolve "
2783 "its runtime-library shader\n";
2784 return;
2785 }
2786 const std::string logical_key = canonical_logical.string();
2787 const fs::path previously_resolved =
2788 resolvedShaderPath(logical_shader);
2789
2790 mxvk::ShaderModuleInfo module_info;
2791 try {
2792 input::validate_spirv_file(requested_path,
2793 "interface shader reload");
2794 module_info = mxvk::inspect_spirv(
2795 mxvk::load_spv(requested_path.string()));
2796 } catch (const std::exception &reload_error) {
2797 std::cerr << "acmxvk: rejected compiled shader reload: "
2798 << reload_error.what() << '\n';
2799 return;
2800 }
2801
2802 const bool history_before = shader_history_required;
2803 const bool spectrum_before = shader_spectrum_required;
2804 const bool spectrum_history_before =
2806 recordShaderResources(module_info, "live shader reload");
2807 const bool resources_grew =
2808 history_before != shader_history_required ||
2809 spectrum_before != shader_spectrum_required ||
2810 spectrum_history_before !=
2812
2813 const std::vector<fs::path> active_pipeline = activeShaderPipeline();
2814 std::error_code active_error;
2815 const fs::path canonical_previous =
2816 fs::weakly_canonical(previously_resolved, active_error);
2817 const bool active = !active_error &&
2818 std::any_of(active_pipeline.begin(), active_pipeline.end(),
2819 [&](const fs::path &shader) {
2820 std::error_code shader_error;
2821 return fs::weakly_canonical(shader, shader_error) ==
2822 canonical_previous &&
2823 !shader_error;
2824 });
2825 if (requested_path == canonical_logical)
2826 shader_reload_overrides.erase(logical_key);
2827 else
2828 shader_reload_overrides[logical_key] = requested_path;
2829 if (active && frame_sprite != nullptr) {
2830 model_effect_shader.clear();
2831 if (resources_grew) {
2833 } else {
2836 }
2837 std::cout << "acmxvk: live reloaded active "
2838 << (module_info.stage == mxvk::ShaderStage::Compute
2839 ? "compute"
2840 : "fragment")
2841 << " shader: " << requested_path.string() << '\n';
2842 } else {
2843 std::cout << "acmxvk: live compiled shader ready for its next "
2844 "use: "
2845 << requested_path.string() << '\n';
2846 }
2847 }
2848
2850 const InterfaceMultipassState &requested) {
2851 std::vector<fs::path> requested_passes;
2852 if (requested.enabled) {
2853 if (requested.shader_names.empty()) {
2854 std::cerr << "acmxvk: rejected enabled interface multipass "
2855 "state without any shader passes\n";
2856 return;
2857 }
2858 requested_passes.reserve(requested.shader_names.size());
2859 for (const std::string &name : requested.shader_names) {
2860 const fs::path requested_path(name);
2861 const bool has_parent_reference = std::any_of(
2862 requested_path.begin(), requested_path.end(),
2863 [](const fs::path &part) { return part == ".."; });
2864 if (requested_path.is_absolute() || has_parent_reference) {
2865 std::cerr << "acmxvk: rejected unsafe interface "
2866 "multipass shader name: "
2867 << name << '\n';
2868 return;
2869 }
2870 const fs::path shader = find_shader_path(
2872 if (shader.empty()) {
2873 std::cerr << "acmxvk: interface multipass shader is not "
2874 "in the active library: "
2875 << name << '\n';
2876 return;
2877 }
2878 requested_passes.push_back(shader);
2879 }
2880 }
2881
2882 const bool requested_enabled =
2883 requested.enabled && !requested_passes.empty();
2884 if (multipass_enabled == requested_enabled &&
2885 configured_passes == requested_passes) {
2886 return;
2887 }
2888 if (frame_sprite != nullptr && shader_locked) {
2889 std::cerr << "acmxvk: interface multipass update ignored while "
2890 "shader switching is locked\n";
2891 return;
2892 }
2893
2894 if (frame_sprite != nullptr) {
2896 }
2897 configured_passes = std::move(requested_passes);
2898 multipass_enabled = requested_enabled;
2899 if (frame_sprite != nullptr) {
2903 }
2904
2905 if (multipass_enabled) {
2906 std::cout << "acmxvk: interface multipass enabled ("
2907 << configured_passes.size() << " passes)";
2908 for (const fs::path &shader : configured_passes) {
2909 std::cout << "\n " << shader.filename().string();
2910 }
2911 std::cout << '\n';
2912 } else {
2913 std::cout << "acmxvk: interface multipass disabled\n";
2914 }
2915 }
2916
2918 const std::string &requested_name) {
2919 if (requested_name.empty()) {
2920 return;
2921 }
2922
2923 const fs::path requested(requested_name);
2924 const bool has_parent_reference =
2925 std::any_of(requested.begin(), requested.end(),
2926 [](const fs::path &part) { return part == ".."; });
2927 if (requested.is_absolute() || has_parent_reference) {
2928 std::cerr << "acmxvk: rejected unsafe interface shader name: "
2929 << requested_name << '\n';
2930 return;
2931 }
2932
2933 const fs::path shader = find_shader_path(
2934 shaders, shader_library_directory, requested_name);
2935 const auto match = std::find(shaders.begin(), shaders.end(), shader);
2936 if (shader.empty() || match == shaders.end()) {
2937 std::cerr << "acmxvk: interface shader is not in the active "
2938 "library: "
2939 << requested_name << '\n';
2940 return;
2941 }
2942
2943 const std::size_t next_index =
2944 static_cast<std::size_t>(std::distance(shaders.begin(), match));
2945 if (next_index == shader_index) {
2946 return;
2947 }
2948 if (shader_locked || frame_sprite == nullptr) {
2949 std::cerr << "acmxvk: interface shader selection ignored while "
2950 "shader switching is locked\n";
2951 return;
2952 }
2953
2955 shader_index = next_index;
2959 std::cout << "acmxvk: interface selected " << activeShaderRole()
2960 << ' ' << (shader_index + 1) << '/' << shaders.size()
2961 << ": " << currentShader() << '\n';
2962 }
2963
2965 const std::vector<InterfaceUniformValue> &uniform_values) {
2966 if (uniform_values.empty()) {
2967 return;
2968 }
2969
2970 std::size_t changed_count = 0;
2971 std::size_t ignored_count = 0;
2972 for (const InterfaceUniformValue &incoming : uniform_values) {
2973 if (!isValidCustomUniformName(incoming.name) ||
2974 !std::isfinite(incoming.value)) {
2975 ++ignored_count;
2976 continue;
2977 }
2978 const auto match = std::find_if(
2979 custom_uniforms.begin(), custom_uniforms.end(),
2980 [&](const ShaderManifest::CustomUniform &uniform) {
2981 return uniform.name == incoming.name;
2982 });
2983 if (match == custom_uniforms.end()) {
2984 ++ignored_count;
2985 continue;
2986 }
2987 const std::size_t index = static_cast<std::size_t>(
2988 std::distance(custom_uniforms.begin(), match));
2989 if (index >= custom_uniform_values.size()) {
2990 ++ignored_count;
2991 continue;
2992 }
2993 const float value = static_cast<float>(std::clamp(
2994 static_cast<double>(incoming.value), match->minimum,
2995 match->maximum));
2996 if (custom_uniform_values[index] == value) {
2997 continue;
2998 }
2999 custom_uniform_values[index] = value;
3000 ++changed_count;
3001 }
3002
3003 if (changed_count > 0) {
3005 std::cout << "acmxvk: interface updated " << changed_count
3006 << " custom uniform(s)\n";
3007 }
3008 if (ignored_count > 0) {
3009 std::cerr << "acmxvk: interface ignored " << ignored_count
3010 << " unknown or invalid custom uniform(s)\n";
3011 }
3012 }
3013
3015 for (const int index : options.shader_pass_indices) {
3016 if (index < 0 || index >= static_cast<int>(shaders.size())) {
3017 throw std::runtime_error("shader pass index is out of range: " +
3018 std::to_string(index));
3019 }
3020 configured_passes.push_back(shaders[static_cast<std::size_t>(index)]);
3021 }
3022 for (const std::string &name : options.shader_pass_files) {
3023 const fs::path shader = find_shader_path(
3025 if (shader.empty()) {
3026 throw std::runtime_error("shader pass file is not listed in the manifest: " +
3027 name);
3028 }
3029 configured_passes.push_back(shader);
3030 }
3032 }
3033
3035 if (options.playlist_file.empty()) {
3036 return;
3037 }
3038 playlist = load_playlist(options.playlist_file, shaders,
3039 shader_library_directory, std::cerr);
3040 playlist_enabled = options.enable_playlist;
3041 std::cout << "acmxvk: playlist loaded "
3042 << playlist_shader_count(playlist) << " shaders in "
3043 << playlist.size() << " nodes from "
3044 << options.playlist_file << '\n';
3045 logSelectedPlaylistNode("selected");
3046 }
3047 // Resource resolution, HUD/watermark drawing, and DNN overlays.
3049 const auto resolve = [&](std::string &path,
3050 const fs::path &resource_subdirectory,
3051 std::string_view label) {
3052 if (path.empty() || fs::is_regular_file(path) ||
3053 fs::path(path).is_absolute()) {
3054 return;
3055 }
3056 fs::path resolved = find_resource(options, fs::path(path));
3057 if (resolved.empty()) {
3058 resolved = find_resource(
3059 options, resource_subdirectory / fs::path(path));
3060 }
3061 if (!resolved.empty()) {
3062 path = resolved.string();
3063 std::cout << "acmxvk: " << label << " (resource path): "
3064 << path << '\n';
3065 }
3066 };
3067 resolve(options.playlist_file, "playlists", "playlist");
3068 resolve(options.midi_map_file, "midi-examples", "MIDI map");
3069 if (options.enable_3d) {
3070 if (options.model_file.empty()) {
3071 options.model_file = default_model_path(options).string();
3072 std::cout << "acmxvk: 3D model (default): "
3073 << options.model_file << '\n';
3074 } else {
3075 resolve(options.model_file, "models", "3D model");
3076 }
3077
3078 std::string model_name =
3079 fs::path(options.model_file).filename().string();
3080 std::transform(
3081 model_name.begin(), model_name.end(), model_name.begin(),
3082 [](unsigned char character) {
3083 return static_cast<char>(std::tolower(character));
3084 });
3085 if (!model_name.ends_with(".obj") &&
3086 !model_name.ends_with(".mxmod") &&
3087 !model_name.ends_with(".mxmod.z")) {
3088 throw std::runtime_error(
3089 "--model requires an .obj, .mxmod, or .mxmod.z file");
3090 }
3091 if (!fs::is_regular_file(options.model_file)) {
3092 throw std::runtime_error(
3093 "3D model was not found: " + options.model_file);
3094 }
3095 constexpr std::uintmax_t MAX_MODEL_BYTES =
3096 1024U * 1024U * 1024U;
3097 input::validate_file_size(options.model_file, "3D model",
3098 MAX_MODEL_BYTES);
3099 }
3100 }
3101
3103 if (counter_disabled && !options.display_filter &&
3104 options.watermark_text.empty() && !options.interface_shm) {
3105 return;
3106 }
3107
3108 const fs::path font = overlay_font_path(options);
3109 if (!fs::is_regular_file(font)) {
3110 throw std::runtime_error("overlay font was not found: " +
3111 font.string());
3112 }
3113 const VkExtent2D preview_extent = getSwapchainExtent();
3114 const int preview_height = preview_extent.height > 0U
3115 ? static_cast<int>(preview_extent.height)
3116 : options.height;
3117 constexpr int FONT_HEIGHT_DIVISOR = 60;
3119 std::max(12, preview_height / FONT_HEIGHT_DIVISOR);
3121 setFont(font.string(), overlay_font_size);
3122 setPreviewFont(font.string(), preview_overlay_font_size);
3123 std::cout << "acmxvk: window-scaled output/HUD font "
3124 << font.string() << " at " << overlay_font_size
3125 << " points\n";
3126 }
3127
3128 [[nodiscard]] std::string MainWindow::clipOverlayText(std::string text) {
3129 constexpr std::size_t MAX_OVERLAY_CHARACTERS = 120;
3130 return input::truncate_utf8(text, MAX_OVERLAY_CHARACTERS);
3131 }
3132
3133 [[nodiscard]] const std::vector<fs::path> *MainWindow::activePasses() const {
3134 if (playlist_enabled && !playlist.empty()) {
3135 return &playlist[playlist_index].shaders;
3136 }
3137 if (multipass_enabled && !configured_passes.empty()) {
3138 return &configured_passes;
3139 }
3140 return nullptr;
3141 }
3142
3143 [[nodiscard]] std::string_view MainWindow::activeShaderRole() const {
3144 const std::vector<fs::path> *passes = activePasses();
3145 return passes != nullptr && !passes->empty() ? "Post-shader"
3146 : "Shader";
3147 }
3148
3149 [[nodiscard]] std::string MainWindow::activePassDescription() const {
3150 const std::vector<fs::path> *passes = activePasses();
3151 if (passes == nullptr || passes->empty()) {
3152 return {};
3153 }
3154
3155 std::string description = "Multipass: ";
3156 for (std::size_t index = 0; index < passes->size(); ++index) {
3157 if (index > 0U) {
3158 description += ", ";
3159 }
3160 description += (*passes)[index].filename().string();
3161 }
3162 return clipOverlayText(std::move(description));
3163 }
3164
3165 [[nodiscard]] std::string MainWindow::activePlaylistDescription() const {
3166 if (!playlist_enabled || playlist.empty()) {
3167 return {};
3168 }
3169 std::ostringstream description;
3170 description << "Playlist [" << (playlist_index + 1) << '/'
3171 << playlist.size() << "]: "
3172 << playlist[playlist_index].name;
3173 return clipOverlayText(description.str());
3174 }
3175
3176 [[nodiscard]] std::string MainWindow::formatHudTime(double seconds_value) {
3177 const double finite_seconds =
3178 std::isfinite(seconds_value) ? seconds_value : 0.0;
3179 const auto elapsed = static_cast<std::uint64_t>(
3180 std::floor(std::max(0.0, finite_seconds)));
3181 const std::uint64_t hours = elapsed / 3600U;
3182 const std::uint64_t minutes = (elapsed / 60U) % 60U;
3183 const std::uint64_t seconds = elapsed % 60U;
3184 std::ostringstream text;
3185 text << std::setfill('0') << std::setw(2) << hours << ':'
3186 << std::setw(2) << minutes << ':' << std::setw(2) << seconds;
3187 return text.str();
3188 }
3189
3191 SDL_Window *window = getSDLWindow();
3192 if (window == nullptr) {
3193 return;
3194 }
3195
3196 const auto now = std::chrono::steady_clock::now();
3197 constexpr auto UPDATE_INTERVAL = std::chrono::milliseconds(500);
3198 if (!force && window_title_last_update.time_since_epoch().count() != 0 &&
3199 now - window_title_last_update < UPDATE_INTERVAL) {
3200 return;
3201 }
3203
3204 const bool recording = writer.is_open() || options.png_output;
3205 double elapsed_seconds = hudWallElapsedSeconds();
3206 std::uint64_t displayed_frames = frame_count;
3207 if (recording && recording_fps > 0.0) {
3208 displayed_frames = output_frame_count;
3209 elapsed_seconds = writer.is_open()
3210 ? writer.get_duration()
3211 : static_cast<double>(output_frame_count) /
3213 } else if (source_kind == SourceKind::Video) {
3214 displayed_frames = video_source_frame_count;
3215 elapsed_seconds = hudVideoPositionSeconds();
3216 }
3217
3218 std::ostringstream title;
3220 title << "ACMXVK - Graphics Mode - "
3221 << formatHudTime(elapsed_seconds) << " ["
3222 << displayed_frames << " frames]";
3223 } else if (source_kind == SourceKind::Video) {
3224 const std::uint64_t total_frames =
3226 ? static_cast<std::uint64_t>(std::llround(
3228 : 0U;
3229 title << "ACMXVK - [" << video_source_frame_count << '/';
3230 if (total_frames > 0U) {
3231 title << total_frames;
3232 } else {
3233 title << '?';
3234 }
3235 title << "] - " << formatHudTime(elapsed_seconds)
3236 << " - Video Mode";
3237 } else {
3238 title << "ACMXVK - Capture Mode - "
3239 << formatHudTime(elapsed_seconds) << " ["
3240 << displayed_frames << " frames]";
3241 }
3242
3243 if (recording) {
3244 title << " (Recording)";
3245 if (writer.is_open()) {
3246 constexpr double BYTES_PER_MEGABYTE = 1024.0 * 1024.0;
3247 const double file_size_mb =
3248 static_cast<double>(writer.get_bytes_written()) /
3249 BYTES_PER_MEGABYTE;
3250 title << " [File: " << std::fixed << std::setprecision(2)
3251 << file_size_mb << " MB]";
3252 }
3253 } else {
3254 title << " (Preview)";
3255 }
3256
3257 const std::string text = title.str();
3258 SDL_SetWindowTitle(window, text.c_str());
3259 }
3260
3262 if (!options.headless || recording_fps <= 0.0 ||
3263 output_frame_count == 0U) {
3264 return;
3265 }
3266
3267 std::uint64_t expected_frames = 0U;
3268 if (options.duration > 0.0) {
3269 const auto duration_frames = static_cast<std::uint64_t>(
3270 std::ceil(options.duration * recording_fps));
3271 expected_frames = std::max<std::uint64_t>(1U, duration_frames);
3272 }
3274 video_duration_seconds > 0.0) {
3275 const auto source_frames = static_cast<std::uint64_t>(
3277 if (expected_frames == 0U) {
3278 expected_frames = source_frames;
3279 } else if (!options.repeat) {
3280 expected_frames = std::min(expected_frames, source_frames);
3281 }
3282 }
3283 if (complete && expected_frames == 0U) {
3284 expected_frames = output_frame_count;
3285 }
3286
3287 const auto now = std::chrono::steady_clock::now();
3288 int percent = -1;
3289 if (expected_frames > 0U) {
3290 const std::uint64_t processed_frames = complete
3291 ? expected_frames
3292 : std::min(
3294 expected_frames);
3295 percent = static_cast<int>(
3296 (static_cast<double>(processed_frames) /
3297 static_cast<double>(expected_frames)) *
3298 100.0);
3299 if (!complete) {
3300 percent = std::min(percent, 99);
3301 }
3302 }
3303
3304 const bool percent_changed =
3305 percent >= 0 && percent > headless_progress_last_percent;
3306 const bool time_elapsed =
3307 headless_progress_last_emit.time_since_epoch().count() == 0 ||
3309 std::chrono::milliseconds(500);
3310 if (!complete && !percent_changed && !time_elapsed) {
3311 return;
3312 }
3313
3316 const std::uint64_t processed_frames =
3317 complete && expected_frames > 0U ? expected_frames
3319 const std::uint64_t written_frames =
3320 writer.is_open()
3321 ? static_cast<std::uint64_t>(
3322 std::max<std::int64_t>(0, writer.get_frame_count()))
3324 const double elapsed_seconds =
3325 static_cast<double>(processed_frames) / recording_fps;
3326
3327 std::cout << "acmxvk: [";
3328 if (percent >= 0) {
3329 std::cout << std::setw(3) << percent << '%';
3330 } else {
3331 std::cout << " ?%";
3332 }
3333 std::cout << "] Frame " << processed_frames << '/';
3334 if (expected_frames > 0U) {
3335 std::cout << expected_frames;
3336 } else {
3337 std::cout << '?';
3338 }
3339 std::cout << " | Written: " << written_frames
3340 << " | Time: " << formatHudTime(elapsed_seconds);
3341 if (writer.is_open()) {
3342 constexpr double BYTES_PER_MEGABYTE = 1024.0 * 1024.0;
3343 const double file_size_mb =
3344 static_cast<double>(writer.get_bytes_written()) /
3345 BYTES_PER_MEGABYTE;
3346 std::ostringstream size_text;
3347 size_text << std::fixed << std::setprecision(2) << file_size_mb;
3348 std::cout << " | Size: " << size_text.str() << " MB";
3349 }
3350 std::cout << '\n'
3351 << std::flush;
3352 }
3353
3354 [[nodiscard]] double MainWindow::hudWallElapsedSeconds() const {
3355 return std::max(
3356 0.0,
3357 std::chrono::duration<double>(std::chrono::steady_clock::now() -
3359 .count());
3360 }
3361
3363 double &timeline,
3364 std::uint64_t *frame_index) const {
3367 !std::isfinite(video_source_fps) || video_source_fps <= 0.0) {
3368 return false;
3369 }
3370 const std::uint64_t index = video_source_frame_count - 1U;
3371 timeline = static_cast<double>(index) / video_source_fps;
3372 if (frame_index != nullptr) {
3373 *frame_index = index;
3374 }
3375 return true;
3376 }
3377
3378 [[nodiscard]] double MainWindow::hudVideoPositionSeconds() const {
3379 double position = 0.0;
3380 if (!currentVideoTimeline(position)) {
3381 return 0.0;
3382 }
3383 if (video_duration_seconds > 0.0) {
3384 position = std::min(position, video_duration_seconds);
3385 }
3386 return std::max(0.0, position);
3387 }
3388
3389 [[nodiscard]] std::string MainWindow::hudVideoTimeString() const {
3390 std::string text = "Video: " +
3392 " / ";
3393 text += video_duration_seconds > 0.0
3395 : "--:--:--";
3396 return text;
3397 }
3398
3399 [[nodiscard]] std::string MainWindow::hudElapsedTimeString() const {
3400 return "Elapsed: " + formatHudTime(hudWallElapsedSeconds());
3401 }
3402
3405 const auto now = std::chrono::steady_clock::now();
3406 const double elapsed =
3407 std::chrono::duration<double>(now - hud_fps_last_tick).count();
3408 if (elapsed < 0.5) {
3409 return;
3410 }
3411 hud_display_fps = static_cast<double>(hud_fps_frame_count) / elapsed;
3413 hud_fps_last_tick = now;
3414 }
3415
3417 if (!options.maximize_fps || options.requested_fps <= 0.0) {
3418 return;
3419 }
3420
3421 const auto interval = std::chrono::duration_cast<
3422 std::chrono::steady_clock::duration>(
3423 std::chrono::duration<double>(1.0 / options.requested_fps));
3424 const auto now = std::chrono::steady_clock::now();
3425 if (!render_pacing_started) {
3426 render_pacing_started = true;
3427 next_render_tick = now;
3428 return;
3429 }
3430
3431 next_render_tick += interval;
3432 if (next_render_tick > now) {
3433 std::this_thread::sleep_until(next_render_tick);
3434 return;
3435 }
3436
3437 if (now - next_render_tick > interval * 4) {
3438 next_render_tick = now;
3439 }
3440 }
3441
3444 return;
3445 }
3446
3447 const auto now = std::chrono::steady_clock::now();
3448 if (camera_fps_frame_count == 0) {
3451 return;
3452 }
3453
3455 const double elapsed = std::chrono::duration<double>(
3457 .count();
3458 if (elapsed < 1.0) {
3459 return;
3460 }
3461
3463 static_cast<double>(camera_fps_frame_count - 1) / elapsed;
3466
3467 const double log_threshold = std::max(
3468 5.0, camera_last_logged_fps * 0.2);
3469 if (camera_last_logged_fps <= 0.0 ||
3471 log_threshold) {
3472 std::ostringstream status;
3473 status << "acmxvk: camera delivery: " << std::fixed
3474 << std::setprecision(1) << camera_delivered_fps
3475 << " FPS measured";
3476 if (camera_reported_fps > 0.0) {
3477 status << " (driver reports " << camera_reported_fps
3478 << " FPS)";
3479 }
3480 std::cout << status.str() << '\n';
3482 }
3483 }
3484
3485 void MainWindow::queueRuntimeHud(int &y, int line_height) {
3486 if (counter_disabled) {
3487 return;
3488 }
3490
3491 const SDL_Color shader_color{0U, 96U, 255U, 255U};
3492 std::string shader = effects_enabled
3493 ? fs::path(currentShader()).filename().string()
3494 : "bypassed";
3495 if (shader_locked) {
3496 shader += " [locked]";
3497 }
3498 printPreviewText(clipOverlayText(
3499 std::string(activeShaderRole()) + ": " +
3500 std::move(shader)),
3501 10, y, shader_color);
3502 y += line_height;
3503
3504 const SDL_Color crossfade_color{255U, 192U, 0U, 255U};
3505 std::ostringstream crossfade_status;
3506 crossfade_status << "XFade [" << (crossfade_shader_index + 1)
3507 << '/' << CROSSFADE_NAMES.size() << "]: "
3509 printPreviewText(clipOverlayText(crossfade_status.str()), 10, y,
3510 crossfade_color);
3511 y += line_height;
3512
3513 const std::string playlist_description =
3515 if (!playlist_description.empty()) {
3516 const SDL_Color playlist_color{255U, 0U, 255U, 255U};
3517 printPreviewText(playlist_description, 10, y,
3518 playlist_color);
3519 y += line_height;
3520 }
3521
3522 const std::vector<fs::path> *passes = activePasses();
3523 if (passes != nullptr && !passes->empty()) {
3524 constexpr std::size_t MAX_HUD_PASS_LINES = 8U;
3525 const std::size_t displayed_passes =
3526 std::min(passes->size(), MAX_HUD_PASS_LINES);
3527 for (std::size_t index = 0; index < displayed_passes;
3528 ++index) {
3529 std::ostringstream pass;
3530 pass << "Pass [" << (index + 1) << '/' << passes->size()
3531 << "]: " << (*passes)[index].filename().string();
3532 printPreviewText(clipOverlayText(pass.str()), 10, y,
3533 shader_color);
3534 y += line_height;
3535 }
3536 if (displayed_passes < passes->size()) {
3537 const std::string remaining =
3538 "Passes: +" +
3539 std::to_string(passes->size() - displayed_passes) +
3540 " more";
3541 printPreviewText(remaining, 10, y, shader_color);
3542 y += line_height;
3543 }
3544 }
3545
3546 if (model_initialized) {
3547 const SDL_Color model_color{0U, 220U, 180U, 255U};
3548 std::string model_status =
3549 model_3d_active ? "Model: " : "Model (2D bypass): ";
3550 model_status +=
3551 fs::path(options.model_file).filename().string();
3552 if (model_wave_active) {
3553 model_status += " [wave]";
3554 }
3556 model_status += " [oscillate]";
3557 }
3558 printPreviewText(clipOverlayText(std::move(model_status)), 10,
3559 y, model_color);
3560 y += line_height;
3561 }
3562
3563#ifdef ACMXVK_WITH_DNN
3564 const SDL_Color dnn_color{64U, 220U, 128U, 255U};
3565 if (human_segmenter != nullptr) {
3566 printPreviewText(
3567 options.human_background
3568 ? "DNN: PP-HumanSeg [background]"
3569 : "DNN: PP-HumanSeg [foreground]",
3570 10, y, dnn_color);
3571 y += line_height;
3572 }
3573 if (edge_detector != nullptr) {
3574 printPreviewText("DNN: DexiNed edge", 10, y, dnn_color);
3575 y += line_height;
3576 }
3577 if (generic_onnx_processor != nullptr) {
3578 printPreviewText(
3580 "DNN: ONNX " +
3581 fs::path(options.onnx_configuration)
3582 .filename()
3583 .string()),
3584 10, y, dnn_color);
3585 y += line_height;
3586 }
3587#endif
3588
3589#ifdef AUDIO_ENABLED
3590 if (file_audio_source != nullptr && file_audio_source->is_open()) {
3591 const std::string track = fs::path(
3593 ->current_track_path())
3594 .filename()
3595 .string();
3596 if (!track.empty()) {
3597 const SDL_Color track_color{255U, 0U, 255U, 255U};
3598 printPreviewText(clipOverlayText("Track: " + track), 10,
3599 y, track_color);
3600 y += line_height;
3601 }
3602 }
3603#endif
3604
3605#ifdef ACMXVK_WITH_CUDA
3606 if (gpu_filter_engine != nullptr) {
3607 const SDL_Color gpu_color{255U, 0U, 255U, 255U};
3608 printPreviewText(
3610 "GPU: " +
3611 gpu_filter_engine->active_filter_description()),
3612 10, y, gpu_color);
3613 y += line_height;
3614 }
3615#endif
3616
3617 if (autopilot_enabled) {
3618 const int remaining =
3620 std::ostringstream status;
3621 status << "Autopilot "
3622 << (autopilot_sequential ? "seq" : "rnd") << ' ';
3623 if (options.autopilot_random_timeout > 0) {
3624 status << "[4-" << options.autopilot_random_timeout
3625 << "] cur=" << autopilot_interval_frames;
3626 } else {
3627 status << "every " << autopilot_interval_frames << 'f';
3628 }
3629 status << " next=" << remaining << "f";
3630 if (!playlist.empty()) {
3631 status << " idx=" << (playlist_index + 1) << '/'
3632 << playlist.size();
3633 }
3634 const SDL_Color autopilot_color{0U, 255U, 255U, 255U};
3635 printPreviewText(clipOverlayText(status.str()), 10, y,
3636 autopilot_color);
3637 y += line_height;
3638 }
3639
3640 const SDL_Color status_color{255U, 255U, 255U, 255U};
3642 printPreviewText(hudVideoTimeString(), 10, y, status_color);
3643 y += line_height;
3644 }
3645 printPreviewText(hudElapsedTimeString(), 10, y, status_color);
3646 y += line_height;
3647 std::ostringstream fps;
3648 fps << "Render: " << std::fixed << std::setprecision(1)
3649 << hud_display_fps << " FPS";
3650 printPreviewText(fps.str(), 10, y, status_color);
3651 y += line_height;
3653 std::ostringstream camera_fps;
3654 camera_fps << "Camera: ";
3655 if (camera_delivered_fps > 0.0) {
3656 camera_fps << std::fixed << std::setprecision(1)
3657 << camera_delivered_fps << " FPS measured";
3658 } else {
3659 camera_fps << "measuring...";
3660 }
3661 printPreviewText(camera_fps.str(), 10, y, status_color);
3662 y += line_height;
3663 }
3664 const SDL_Color hint_color{128U, 128U, 128U, 255U};
3665 printPreviewText("F9: Toggle overlay", 10, y, hint_color);
3666 y += line_height;
3667 }
3668
3670 if (counter_disabled && !options.display_filter &&
3671 (!watermark_enabled || options.watermark_text.empty())) {
3672 return;
3673 }
3674
3675 constexpr int LEFT_MARGIN = 10;
3676 constexpr int TOP_MARGIN = 10;
3677 const int line_height = overlay_font_size + 4;
3678 const int preview_line_height = preview_overlay_font_size + 4;
3679 int preview_y =
3680 TOP_MARGIN +
3682 !options.watermark_text.empty()
3683 ? preview_line_height
3684 : 0);
3685 queueRuntimeHud(preview_y, preview_line_height);
3686 int y = TOP_MARGIN;
3687 if (options.display_filter) {
3688 const SDL_Color filter_color{255U, 0U, 255U, 255U};
3689 std::string shader = effects_enabled
3690 ? fs::path(currentShader()).filename().string()
3691 : "bypassed";
3692 printText(clipOverlayText(
3693 std::string(activeShaderRole()) + ": " +
3694 std::move(shader)),
3695 LEFT_MARGIN, y, filter_color);
3696 y += line_height;
3697
3698 if (playlist_enabled && !playlist.empty()) {
3699 printText(clipOverlayText("Playlist: " +
3700 playlist[playlist_index].name),
3701 LEFT_MARGIN, y, filter_color);
3702 y += line_height;
3703 }
3704 const std::string passes = activePassDescription();
3705 if (!passes.empty()) {
3706 printText(passes, LEFT_MARGIN, y, filter_color);
3707 y += line_height;
3708 }
3709#ifdef ACMXVK_WITH_CUDA
3710 if (gpu_filter_engine != nullptr) {
3711 printText(clipOverlayText(
3712 "GPU: " + gpu_filter_engine
3713 ->active_filter_description()),
3714 LEFT_MARGIN, y, filter_color);
3715 y += line_height;
3716 }
3717#endif
3718 }
3719
3720 if (watermark_enabled && !options.watermark_text.empty()) {
3721 const SDL_Color watermark_color{
3722 options.watermark_color[0], options.watermark_color[1],
3723 options.watermark_color[2], 255U};
3724 printText(clipOverlayText(options.watermark_text), LEFT_MARGIN,
3725 y, watermark_color);
3726 }
3727 }
3728
3729 [[nodiscard]] std::string MainWindow::captureFourccName(double value) {
3730 if (!std::isfinite(value) || value <= 0.0 ||
3731 value > static_cast<double>(
3732 std::numeric_limits<std::uint32_t>::max())) {
3733 return "unknown";
3734 }
3735 const auto fourcc = static_cast<std::uint32_t>(std::llround(value));
3736 std::string name(4, ' ');
3737 for (std::size_t index = 0; index < name.size(); ++index) {
3738 const auto byte = static_cast<unsigned char>(
3739 (fourcc >> (index * 8U)) & 0xffU);
3740 if (!std::isprint(byte)) {
3741 return "unknown";
3742 }
3743 name[index] = static_cast<char>(byte);
3744 }
3745 return name;
3746 }
3747
3748 [[nodiscard]] bool MainWindow::hostPreprocessingEnabled() const {
3749#ifdef ACMXVK_WITH_DEEP_DREAM
3750 if (deep_dream_model != nullptr) {
3751 return true;
3752 }
3753#endif
3754#ifdef ACMXVK_WITH_DNN
3755 return edge_detector != nullptr || human_segmenter != nullptr ||
3756 generic_onnx_processor != nullptr;
3757#else
3758 return false;
3759#endif
3760 }
3761
3763#ifdef ACMXVK_WITH_DEEP_DREAM
3764 if (deep_dream_model == nullptr ||
3765 !options.random_dream_specified) {
3766 return;
3767 }
3768
3769 double timeline = 0.0;
3770 if (!currentVideoTimeline(timeline)) {
3771 timeline = hudWallElapsedSeconds();
3772 }
3773 if (!std::isfinite(timeline) || timeline < 0.0) {
3774 timeline = 0.0;
3775 }
3777 timeline < previous_random_dream_timeline) {
3779 std::numeric_limits<std::uint64_t>::max();
3781 }
3783
3784 const auto period = static_cast<std::uint64_t>(
3785 std::floor(timeline / options.random_dream_interval));
3786 if (period == random_dream_period) {
3787 return;
3788 }
3789 random_dream_period = period;
3790
3791 std::uniform_real_distribution<double> strength(0.01, 0.05);
3792 std::uniform_real_distribution<double> feedback(0.55, 0.90);
3793 std::uniform_real_distribution<double> zoom(0.985, 1.015);
3794 std::uniform_real_distribution<double> rotation_magnitude(0.5, 3.0);
3795 std::uniform_real_distribution<double> octave_scale(1.2, 1.6);
3796 std::uniform_int_distribution<int> rotation_direction(0, 1);
3797 std::uniform_int_distribution<int> octaves(1, 8);
3798
3799 options.dream_strength = strength(random_dream_rng);
3800 options.dream_feedback = feedback(random_dream_rng);
3801 options.dream_zoom = zoom(random_dream_rng);
3802 const double magnitude = rotation_magnitude(random_dream_rng);
3803 options.dream_rotation =
3804 rotation_direction(random_dream_rng) == 0 ? -magnitude : magnitude;
3805 options.dream_octaves = octaves(random_dream_rng);
3806 options.dream_octave_scale = octave_scale(random_dream_rng);
3807
3808 std::cout << "acmxvk: random dream: strength "
3809 << options.dream_strength << ", feedback "
3810 << options.dream_feedback << ", zoom "
3811 << options.dream_zoom << ", rotation "
3812 << options.dream_rotation << ", octaves "
3813 << options.dream_octaves << ", octave scale "
3814 << options.dream_octave_scale << '\n';
3815#endif
3816 }
3817
3819#ifdef ACMXVK_WITH_DEEP_DREAM
3820 if (deep_dream_model == nullptr || rgba.empty()) {
3821 return;
3822 }
3823 if (rgba.type() == CV_16UC4) {
3824 cv::Mat compatible = rgba16ToRgba8(rgba);
3826 std::cout << "acmxvk: Deep Dream uses an RGBA8 compatibility "
3827 "copy for HDR input\n";
3829 }
3830 applyDeepDreamEffect(compatible);
3831 compatible.convertTo(rgba, CV_16UC4, 257.0);
3832 return;
3833 }
3836 try {
3837 result = deep_dream_model->apply_gradient_ascent(
3839 options.dream_iterations,
3840 static_cast<float>(options.dream_strength),
3841 static_cast<float>(options.dream_feedback),
3842 static_cast<float>(options.dream_zoom),
3843 static_cast<float>(options.dream_rotation),
3844 options.dream_size, options.dream_channel,
3845 options.dream_octaves,
3846 static_cast<float>(options.dream_octave_scale),
3847 options.dream_jitter, options.dream_smoothing});
3848 } catch (const std::exception &error) {
3849 handleDeepDreamRuntimeError(error.what());
3850 return;
3851 }
3852 if (!std::isfinite(result.mean_pixel_change)) {
3854 "Deep Dream returned a non-finite processed frame");
3855 return;
3856 }
3858 std::cout << "acmxvk: Deep Dream working frame: "
3859 << result.processed_width << 'x'
3860 << result.processed_height << " -> " << rgba.cols << 'x'
3861 << rgba.rows << " source texture ("
3862 << result.processed_octaves << " octave(s))\n";
3864 }
3865#else
3866 static_cast<void>(rgba);
3867#endif
3868 }
3869
3870 void MainWindow::handleDeepDreamRuntimeError(std::string_view message) {
3871#ifdef ACMXVK_WITH_DEEP_DREAM
3872 std::cerr << "acmxvk: Deep Dream frame failed: " << message
3873 << "; disabling Deep Dream while keeping ACMXVK running\n";
3874 deep_dream_model.reset();
3875#ifdef ACMXVK_WITH_MXVK_CUDA
3876 cuda_dream_rgba.release();
3877#endif
3878 options.dream_model.clear();
3879 options.dream_layer.clear();
3880 options.gpu_filter_before_dream = false;
3882#else
3883 static_cast<void>(message);
3884#endif
3885 }
3886
3887 void MainWindow::applyDnnEffects(cv::Mat &rgba) {
3888#ifdef ACMXVK_WITH_DNN
3889 if (rgba.type() == CV_16UC4 &&
3890 (human_segmenter != nullptr || edge_detector != nullptr ||
3891 generic_onnx_processor != nullptr)) {
3892 cv::Mat compatible = rgba16ToRgba8(rgba);
3894 std::cout
3895 << "acmxvk: HDR increment 2: DNN preprocessing uses an "
3896 "RGBA8 compatibility copy before RGBA16 upload\n";
3898 }
3899 applyDnnEffects(compatible);
3900 compatible.convertTo(rgba, CV_16UC4, 257.0);
3901 return;
3902 }
3903 if (human_segmenter != nullptr && !rgba.empty()) {
3904 cv::Mat bgr;
3905 cv::cvtColor(rgba, bgr, cv::COLOR_RGBA2BGR);
3906 const cv::Mat mask = human_segmenter->infer(bgr);
3907 if (mask.empty()) {
3908 throw std::runtime_error(
3909 "PP-HumanSeg produced an empty person mask");
3910 }
3911 const float black_point =
3912 static_cast<float>(options.human_black_point);
3913 const float white_point =
3914 static_cast<float>(options.human_white_point);
3915 if (options.human_background) {
3916 const cv::Mat alpha = dnn::hardenedAlphaMask(
3917 bgr, mask, black_point, white_point);
3918 cv::cvtColor(bgr, human_overlay_rgba,
3919 cv::COLOR_BGR2RGBA);
3920 std::vector<cv::Mat> overlay_channels;
3921 cv::split(human_overlay_rgba, overlay_channels);
3922 alpha.copyTo(overlay_channels[3]);
3923 cv::merge(overlay_channels, human_overlay_rgba);
3924
3925 const cv::Mat foreground = dnn::isolateBody(
3926 bgr, mask, black_point, white_point);
3927 cv::Mat background;
3928 cv::subtract(bgr, foreground, background);
3929 cv::cvtColor(background, rgba, cv::COLOR_BGR2RGBA);
3930 } else {
3931 const cv::Mat foreground = dnn::isolateBody(
3932 bgr, mask, black_point, white_point);
3933 cv::cvtColor(foreground, rgba, cv::COLOR_BGR2RGBA);
3934 }
3935 }
3936 if (edge_detector != nullptr && !rgba.empty()) {
3937 try {
3938 cv::Mat bgr;
3939 cv::Mat edge;
3940 cv::cvtColor(rgba, bgr, cv::COLOR_RGBA2BGR);
3941 edge_detector->process(bgr, edge);
3942 if (edge.empty()) {
3943 throw std::runtime_error(
3944 "DexiNed produced an empty edge frame");
3945 }
3946 if (edge.channels() == 1) {
3947 cv::cvtColor(edge, rgba, cv::COLOR_GRAY2RGBA);
3948 } else {
3949 cv::cvtColor(edge, rgba, cv::COLOR_BGR2RGBA);
3950 }
3951 } catch (const std::exception &error) {
3952 std::cerr
3953 << "acmxvk: edge inference failed; disabling DNN "
3954 "effect: "
3955 << error.what() << '\n';
3956 edge_detector.reset();
3957 }
3958 }
3959 if (generic_onnx_processor != nullptr && !rgba.empty()) {
3960 try {
3961 cv::Mat bgr;
3962 cv::Mat processed;
3963 cv::cvtColor(rgba, bgr, cv::COLOR_RGBA2BGR);
3964 generic_onnx_processor->process(bgr, processed);
3965 if (processed.empty()) {
3966 throw std::runtime_error(
3967 "generic ONNX model produced an empty frame");
3968 }
3969 if (processed.channels() == 1) {
3970 cv::cvtColor(processed, rgba,
3971 cv::COLOR_GRAY2RGBA);
3972 } else {
3973 cv::cvtColor(processed, rgba,
3974 cv::COLOR_BGR2RGBA);
3975 }
3976 } catch (const std::exception &error) {
3977 std::cerr
3978 << "acmxvk: generic ONNX inference failed; disabling "
3979 "model: "
3980 << error.what() << '\n';
3981 generic_onnx_processor.reset();
3982 }
3983 }
3984#else
3985 static_cast<void>(rgba);
3986#endif
3987 }
3988
3990#ifdef ACMXVK_WITH_DNN
3991 if (!options.human_background || human_overlay_rgba.empty() ||
3992 getDevice() == VK_NULL_HANDLE) {
3993 return;
3994 }
3995 if (human_overlay_sprite == nullptr) {
3996 human_overlay_sprite = createSprite(1, 1);
3997 human_overlay_sprite->enableHistoryTexture(
3998 static_cast<std::uint32_t>(human_overlay_rgba.cols),
3999 static_cast<std::uint32_t>(human_overlay_rgba.rows), 1U);
4000 }
4001 cv::Mat upload = human_overlay_rgba;
4002 cv::Mat flipped;
4003 if (options.flip_output) {
4004 cv::flip(human_overlay_rgba, flipped, 0);
4005 upload = flipped;
4006 }
4007 human_overlay_sprite->updateHistoryTexture(
4008 upload.ptr(), upload.cols, upload.rows,
4009 static_cast<int>(upload.step));
4010#endif
4011 }
4012 // Input setup, output encoding, snapshots, and readback handling.
4014 if (!options.graphic_file.empty()) {
4016 graphic_rgba = loadRgbaImage(options.graphic_file);
4019 rotateFrame(graphic_rgba, options.frame_rotation);
4020 if (!human_overlay_rgba.empty()) {
4021 rotateFrame(human_overlay_rgba, options.frame_rotation);
4022 }
4023 return;
4024 }
4025
4027 bool opened = false;
4029 opened = openVideoCapture();
4030 } else {
4031 opened = capture.open(options.camera_device);
4032 }
4033 if (!opened) {
4034 const std::string source = source_kind == SourceKind::Video
4035 ? options.input_file
4036 : std::to_string(options.camera_device);
4037 throw std::runtime_error("unable to open capture source: " + source);
4038 }
4039
4042 probeVideoDuration(options.input_file);
4044#ifdef MXVK_WITH_FFMPEG_CAPTURE
4046 video_hdr_info.valid && video_hdr_info.hdr &&
4047 using_ffmpeg_capture;
4048#else
4050#endif
4051 if (options.gpu_filter_before_dream &&
4053 throw std::runtime_error(
4054 "--gpu-filter-before-dream does not support HDR input");
4055 }
4058 (video_hdr_info.color_transfer ==
4059 COLOR_TRANSFER_SMPTE2084 ||
4060 video_hdr_info.color_transfer ==
4061 COLOR_TRANSFER_ARIB_STD_B67);
4063 video_hdr_info.color_transfer ==
4064 COLOR_TRANSFER_ARIB_STD_B67;
4065 setHdrRenderIntermediatesEnabled(hdr_input_precision_enabled);
4066 setFrameReadbackRgba16Enabled(hdr_input_precision_enabled);
4067 std::ostringstream timeline;
4068 timeline << "acmxvk: video timeline: " << std::fixed
4069 << std::setprecision(3) << video_source_fps
4070 << " FPS";
4071 if (video_duration_seconds > 0.0) {
4072 timeline << ", " << video_duration_seconds
4073 << " seconds";
4074 } else {
4075 timeline << ", duration unavailable";
4076 }
4077 std::cout << timeline.str() << '\n';
4078 if (video_hdr_info.valid && video_hdr_info.hdr) {
4079 std::cout << "acmxvk: HDR input metadata detected\n";
4082 std::cout << "acmxvk: HDR processing active: native "
4083 "RGBA16 input, RGBA16F effects/history, and "
4084 "normalized RGBA16 Vulkan readback\n";
4086 std::cout
4087 << "acmxvk: HDR transfer: "
4088 << (hdr_transfer_hlg ? "HLG" : "PQ")
4089 << " decoded to linear BT.2020 before effects and "
4090 "encoded after effects\n";
4091 if (!options.headless) {
4092 std::cout
4093 << "acmxvk: HDR preview: presentation-only "
4094 "BT.2020-to-BT.709 SDR tone mapping active; "
4095 "Main10 recording remains unchanged\n";
4096 }
4097 } else {
4098 std::cerr
4099 << "acmxvk: HDR transfer "
4100 << video_hdr_info.color_transfer
4101 << " is not PQ or HLG; preserving transfer-encoded "
4102 "values through the precision path\n";
4103 }
4104 } else {
4105 std::cout
4106 << "acmxvk: HDR precision path unavailable because this "
4107 "build is not using MXVK FFmpeg capture; falling "
4108 "back to RGBA8\n";
4109 }
4110 }
4111 if (options.use_source_fps) {
4112 std::cout
4113 << "acmxvk: source-FPS playback enabled at "
4115 << " FPS; early frames wait and late frames are skipped\n";
4116 }
4117 }
4118
4120 // Match ACMX2's ordering. Some V4L2 drivers renegotiate the
4121 // frame interval when dimensions or pixel format change.
4122 capture.set(cv::CAP_PROP_BUFFERSIZE, 1.0);
4123 capture.set(cv::CAP_PROP_FRAME_WIDTH, options.camera_width);
4124 capture.set(cv::CAP_PROP_FRAME_HEIGHT, options.camera_height);
4125 const int requested_fourcc = options.use_yuv
4126 ? cv::VideoWriter::fourcc(
4127 'Y', 'U', 'Y', 'V')
4128 : cv::VideoWriter::fourcc(
4129 'M', 'J', 'P', 'G');
4130 capture.set(cv::CAP_PROP_FOURCC,
4131 static_cast<double>(requested_fourcc));
4132 if (options.requested_fps > 0.0) {
4133 capture.set(cv::CAP_PROP_FPS, options.requested_fps);
4134 }
4135
4136 camera_reported_width = static_cast<int>(
4137 std::lround(capture.get(cv::CAP_PROP_FRAME_WIDTH)));
4138 camera_reported_height = static_cast<int>(
4139 std::lround(capture.get(cv::CAP_PROP_FRAME_HEIGHT)));
4140 camera_reported_fps = capture.get(cv::CAP_PROP_FPS);
4141 if (!std::isfinite(camera_reported_fps) ||
4142 camera_reported_fps < 0.0) {
4143 camera_reported_fps = 0.0;
4144 }
4145 const std::string reported_fourcc = captureFourccName(
4146 capture.get(cv::CAP_PROP_FOURCC));
4147
4148 std::cout << "acmxvk: camera opened: "
4149 << camera_reported_width << 'x'
4151 if (camera_reported_fps > 0.0) {
4152 std::cout << " at reported " << camera_reported_fps
4153 << " FPS";
4154 } else {
4155 std::cout << " at an unreported frame rate";
4156 }
4157 std::cout << ", format=" << reported_fourcc << '\n';
4158
4159 if (camera_reported_width != options.camera_width ||
4160 camera_reported_height != options.camera_height) {
4161 std::cerr << "acmxvk: camera mode warning: requested "
4162 << options.camera_width << 'x'
4163 << options.camera_height << " but driver reports "
4164 << camera_reported_width << 'x'
4166 << '\n';
4167 }
4168 if (options.requested_fps > 0.0 &&
4169 camera_reported_fps > 0.0 &&
4170 std::abs(camera_reported_fps - options.requested_fps) >
4171 0.05) {
4172 std::cerr << "acmxvk: camera mode warning: requested "
4173 << options.requested_fps
4174 << " FPS but driver reports "
4175 << camera_reported_fps << " FPS\n";
4176 }
4177 const std::string requested_format =
4178 options.use_yuv ? "YUYV" : "MJPG";
4179 if (reported_fourcc != "unknown" &&
4180 reported_fourcc != requested_format) {
4181 std::cerr << "acmxvk: camera mode warning: requested "
4182 << requested_format << " but driver reports "
4183 << reported_fourcc << '\n';
4184 }
4185 if (options.maximize_fps) {
4187 std::cout
4188 << "acmxvk: maximize FPS active: asynchronous camera "
4189 "capture, Vulkan render target "
4190 << options.requested_fps << " FPS\n";
4191 if (options.enable_vsync) {
4192 std::cout
4193 << "acmxvk: maximize FPS note: VSync may cap the "
4194 "render rate to the display refresh\n";
4195 }
4196 }
4197 }
4198 }
4199
4200 [[nodiscard]] std::pair<int, int> MainWindow::source_dimensions() {
4201 int source_width = options.width;
4202 int source_height = options.height;
4204 source_width = graphic_rgba.cols;
4205 source_height = graphic_rgba.rows;
4206 } else {
4207#ifdef MXVK_WITH_FFMPEG_CAPTURE
4208 if (using_ffmpeg_capture) {
4209 source_width = ffmpeg_capture.width();
4210 source_height = ffmpeg_capture.height();
4211 } else
4212#endif
4213 {
4217 source_width = camera_reported_width;
4218 source_height = camera_reported_height;
4219 } else {
4220 source_width = static_cast<int>(
4221 std::lround(capture.get(cv::CAP_PROP_FRAME_WIDTH)));
4222 source_height = static_cast<int>(
4223 std::lround(capture.get(cv::CAP_PROP_FRAME_HEIGHT)));
4224 }
4225 }
4226 if (source_width <= 0 || source_height <= 0) {
4227 source_width = source_kind == SourceKind::Camera
4228 ? options.camera_width
4229 : options.width;
4230 source_height = source_kind == SourceKind::Camera
4231 ? options.camera_height
4232 : options.height;
4233 }
4234 if (rotationSwapsDimensions(options.frame_rotation)) {
4235 std::swap(source_width, source_height);
4236 }
4237 }
4238 return {source_width, source_height};
4239 }
4240
4242 int render_width = options.width;
4243 int render_height = options.height;
4244 if (!options.resolution_specified) {
4245 const auto [source_width, source_height] = source_dimensions();
4246 if (!dimensions_supported(source_width, source_height)) {
4247 throw std::runtime_error(
4248 "input source dimensions are outside the supported range");
4249 }
4250
4251 render_width = source_width;
4252 render_height = source_height;
4253 options.width = render_width;
4254 options.height = render_height;
4255 const char *source_name = source_kind == SourceKind::Video
4256 ? "video"
4258 ? "camera"
4259 : "graphic";
4260 std::cout << "acmxvk: automatic output resolution: "
4261 << render_width << 'x' << render_height << " from "
4262 << source_name;
4263 if (rotationSwapsDimensions(options.frame_rotation)) {
4264 std::cout << " after input rotation";
4265 }
4266 std::cout << '\n';
4267 } else {
4268 std::cout << "acmxvk: requested output resolution: "
4269 << render_width << 'x' << render_height << '\n';
4270 }
4271 setRenderExtent(static_cast<std::uint32_t>(render_width),
4272 static_cast<std::uint32_t>(render_height));
4273
4274 if (options.headless) {
4275 std::cout << "acmxvk: headless output resolution: "
4276 << render_width << 'x' << render_height << '\n';
4277 return;
4278 }
4279
4280 if (options.fullscreen) {
4281 std::cout << "acmxvk: fullscreen presentation uses the display "
4282 "extent without changing the output resolution\n";
4283 return;
4284 }
4285
4286 SDL_Window *window = getSDLWindow();
4287 if (window == nullptr) {
4288 throw std::runtime_error(
4289 "unable to configure preview without an SDL window");
4290 }
4291
4292 int preview_width = render_width;
4293 int preview_height = render_height;
4294 SDL_Rect usable_bounds{};
4295 SDL_DisplayID display = SDL_GetDisplayForWindow(window);
4296 if (display == 0) {
4297 display = SDL_GetPrimaryDisplay();
4298 }
4299 if (display != 0 &&
4300 SDL_GetDisplayUsableBounds(display, &usable_bounds) &&
4301 usable_bounds.w > 0 && usable_bounds.h > 0) {
4302 constexpr double PREVIEW_DISPLAY_FRACTION = 0.9;
4303 const double width_scale =
4304 (static_cast<double>(usable_bounds.w) *
4305 PREVIEW_DISPLAY_FRACTION) /
4306 render_width;
4307 const double height_scale =
4308 (static_cast<double>(usable_bounds.h) *
4309 PREVIEW_DISPLAY_FRACTION) /
4310 render_height;
4311 const double preview_scale =
4312 std::min({1.0, width_scale, height_scale});
4313 preview_width = std::max(
4314 1, static_cast<int>(std::lround(render_width * preview_scale)));
4315 preview_height = std::max(
4316 1, static_cast<int>(std::lround(render_height * preview_scale)));
4317 }
4318
4319 const float render_aspect = static_cast<float>(render_width) /
4320 static_cast<float>(render_height);
4321 if (!SDL_SetWindowAspectRatio(window, render_aspect,
4322 render_aspect)) {
4323 std::cerr << "acmxvk: unable to lock preview aspect ratio: "
4324 << SDL_GetError() << '\n';
4325 }
4326 if (!SDL_SetWindowSize(window, preview_width, preview_height)) {
4327 throw std::runtime_error(
4328 std::string("unable to apply preview resolution: ") +
4329 SDL_GetError());
4330 }
4331 SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED,
4332 SDL_WINDOWPOS_CENTERED);
4333 if (!SDL_SyncWindow(window)) {
4334 std::cerr << "acmxvk: window resize sync warning: "
4335 << SDL_GetError() << '\n';
4336 }
4337
4338 int actual_width = 0;
4339 int actual_height = 0;
4340 SDL_GetWindowSizeInPixels(window, &actual_width, &actual_height);
4341 std::cout << "acmxvk: preview resolution: " << actual_width << 'x'
4342 << actual_height;
4343 if (preview_width != render_width ||
4344 preview_height != render_height) {
4345 std::cout << " (" << render_width << 'x' << render_height
4346 << " output, preview fitted to display)";
4347 }
4348 std::cout << '\n';
4349 }
4350
4351 [[nodiscard]] double MainWindow::outputFrameRate() {
4352 if (options.requested_fps > 0.0) {
4353 return options.requested_fps;
4354 }
4356 double source_fps = 0.0;
4357#ifdef MXVK_WITH_FFMPEG_CAPTURE
4358 if (using_ffmpeg_capture) {
4359 source_fps = ffmpeg_capture.fps();
4360 } else
4361#endif
4362 {
4363 source_fps = capture.get(cv::CAP_PROP_FPS);
4364 }
4365 if (std::isfinite(source_fps) && source_fps > 0.0) {
4366 return source_fps;
4367 }
4368 }
4369 return 30.0;
4370 }
4371
4373#ifndef ACMXVK_WITH_TIFF
4374 if (format == SnapshotFormat::Tiff) {
4375 std::cerr << "acmxvk: TIFF snapshots require a build configured "
4376 "with -DTIFF=ON\n";
4377 return;
4378 }
4379#endif
4380#ifndef ACMXVK_WITH_WEBP
4381 if (format == SnapshotFormat::WebP) {
4382 std::cerr << "acmxvk: WebP snapshots require a build configured "
4383 "with -DWEBP=ON\n";
4384 return;
4385 }
4386#endif
4387 if (snapshot_pending) {
4388 return;
4389 }
4390 if (snapshot_writer.queueFull()) {
4391 std::cerr << "acmxvk: snapshot queue is full; request ignored\n";
4392 return;
4393 }
4394 std::error_code error;
4395 const fs::path directory(options.snapshot_directory);
4396 fs::create_directories(directory, error);
4397 if (error || !fs::is_directory(directory)) {
4398 std::cerr << "acmxvk: unable to create snapshot directory: "
4399 << directory.string() << '\n';
4400 return;
4401 }
4402 if (!snapshot_writer.start()) {
4403 return;
4404 }
4405 snapshot_pending = true;
4406 pending_snapshot_format = format;
4407 setFrameReadbackEnabled(true);
4408 std::cout << "acmxvk: " << SnapshotWriter::formatName(format)
4409 << " snapshot requested\n";
4410 }
4411
4412 [[nodiscard]] bool MainWindow::continuousReadbackEnabled() const {
4413 return writer.is_open() || options.png_output ||
4414 options.generate_interval > 0;
4415 }
4416
4418 if (options.output_file.empty() && options.generate_interval <= 0) {
4419 return;
4420 }
4421
4422 const VkExtent2D extent = getRenderExtent();
4423 if (options.resolution_specified) {
4424 recording_width = extent.width > 0U
4425 ? static_cast<int>(extent.width)
4426 : options.width;
4427 recording_height = extent.height > 0U
4428 ? static_cast<int>(extent.height)
4429 : options.height;
4430 } else {
4431 recording_width = options.width;
4432 recording_height = options.height;
4433 }
4435
4436 if (options.png_output) {
4438 output_frame_directory(options.output_file, "png");
4440 std::cout << "acmxvk: writing PNG sequence to "
4441 << png_output_directory.string() << '\n';
4442 }
4443
4444 if (options.generate_interval > 0) {
4445 if (!options.output_file.empty()) {
4447 output_frame_directory(options.output_file,
4448 "generate");
4449 } else if (!options.input_file.empty()) {
4452 "generate");
4453 } else {
4454 generate_output_directory = "camera-generate";
4455 }
4457 std::cout << "acmxvk: saving every " << options.generate_interval
4458 << "th frame to " << generate_output_directory.string() << '\n';
4459 }
4460
4461 if (!options.output_file.empty() && !options.png_output) {
4462 EncodeOptions encode_options;
4463 encode_options.preset = options.encode_preset;
4464 encode_options.tune = options.encode_tune;
4465 encode_options.crf = options.encode_crf;
4466 encode_options.bit_rate = options.encode_bitrate;
4467 encode_options.codec = options.encode_codec;
4468 encode_options.ffmpeg_options = options.encode_params;
4469 encode_options.realtime = options.encode_realtime;
4470 encode_options.block_when_full = options.no_drop;
4472 if (hdr_output_enabled) {
4473 if ((recording_width & 1) != 0 ||
4474 (recording_height & 1) != 0) {
4475 throw std::runtime_error(
4476 "HDR Main10 output requires even width and height");
4477 }
4478 encode_options.hdr.enabled = true;
4479 encode_options.hdr.color_primaries =
4480 video_hdr_info.color_primaries;
4481 encode_options.hdr.color_trc =
4482 video_hdr_info.color_transfer;
4483 encode_options.hdr.color_space =
4484 video_hdr_info.color_space;
4485 encode_options.hdr.color_range =
4486 video_hdr_info.color_range;
4487 encode_options.hdr.mastering_display =
4488 video_hdr_info.mastering_display;
4489 encode_options.hdr.content_light =
4490 video_hdr_info.content_light;
4491 std::cout
4492 << "acmxvk: HDR output: HEVC Main10 with captured "
4493 << (hdr_transfer_hlg ? "BT.2020/HLG" : "BT.2020/PQ")
4494 << " color metadata (software libx265)\n";
4495 }
4496
4497 if (options.encode_bitrate > 0) {
4498 std::cout << "acmxvk: encoder rate control: target VBR "
4499 << options.encode_bitrate << " bits/s\n";
4500 } else {
4501 std::cout << "acmxvk: encoder rate control: CRF/CQ "
4502 << options.encode_crf << '\n';
4503 }
4504
4505 if (!writer.open(options.output_file, recording_width, recording_height,
4506 static_cast<float>(recording_fps), encode_options)) {
4507 throw std::runtime_error("unable to open output video: " +
4508 options.output_file);
4509 }
4510 writer.set_block_when_full(options.no_drop);
4511 std::cout << "acmxvk: recording " << recording_width << 'x'
4512 << recording_height << " at " << recording_fps << " FPS to "
4513 << options.output_file
4514 << (options.no_drop ? " (no-drop)\n" : "\n");
4515 if (options.mute_output) {
4516 std::cout
4517 << "acmxvk: recorded video audio disabled (--mute-output); "
4518 "reactivity and pass-through remain active\n";
4519 }
4520 }
4521
4522 setFrameReadbackEnabled(true);
4523 }
4524
4526 ReadbackRequest request;
4527 request.snapshot = snapshot_pending;
4532 request.pts = recording_frame_pts;
4533 readback_requests.push_back(request);
4534
4535 if (snapshot_pending) {
4536 snapshot_pending = false;
4537 if (!request.continuous) {
4538 setFrameReadbackEnabled(false);
4539 }
4540 }
4541 }
4542
4543 void MainWindow::onFrameReadback(std::vector<std::uint8_t> &rgba, uint32_t width,
4544 uint32_t height) {
4545 handleFrameReadback(rgba, nullptr, width, height);
4546 }
4547
4549 std::vector<std::uint8_t> &rgba,
4550 const std::vector<std::uint16_t> *rgba16, uint32_t width,
4551 uint32_t height) {
4552 if (readback_requests.empty()) {
4553 std::cerr << "acmxvk: received frame readback without queued metadata\n";
4554 return;
4555 }
4556 const ReadbackRequest request = readback_requests.front();
4557 readback_requests.pop_front();
4558
4559 if (request.snapshot) {
4560 const fs::path path = snapshot_path(
4561 options.snapshot_directory, width, height, snapshot_count,
4562 request.snapshot_format);
4563 SnapshotJob job;
4564 job.path = path;
4565 job.width = width;
4566 job.height = height;
4567 job.format = request.snapshot_format;
4568 if (rgba16 != nullptr &&
4571 job.rgba16 = *rgba16;
4572 }
4573 if (request.continuous) {
4574 job.rgba = rgba;
4575 } else {
4576 job.rgba = std::move(rgba);
4577 }
4578 snapshot_writer.enqueue(std::move(job));
4580 std::cout << "acmxvk: queued "
4582 << " snapshot: " << path.string() << '\n';
4583 }
4584
4585 if (!request.continuous || recording_complete ||
4586 !request.frame_due) {
4587 return;
4588 }
4589
4590 std::uint8_t *output_pixels = rgba.data();
4591 cv::Mat resized;
4592 const std::uint16_t *hdr_output_pixels =
4593 rgba16 != nullptr ? rgba16->data() : nullptr;
4594 cv::Mat hdr_resized;
4595 if (static_cast<int>(width) != recording_width ||
4596 static_cast<int>(height) != recording_height) {
4597 const cv::Mat source(static_cast<int>(height), static_cast<int>(width),
4598 CV_8UC4, rgba.data());
4599 cv::resize(source, resized, cv::Size(recording_width, recording_height),
4600 0.0, 0.0, cv::INTER_LINEAR);
4601 output_pixels = resized.ptr();
4602 if (rgba16 != nullptr) {
4603 const cv::Mat hdr_source(
4604 static_cast<int>(height), static_cast<int>(width),
4605 CV_16UC4,
4606 const_cast<std::uint16_t *>(rgba16->data()));
4607 cv::resize(hdr_source, hdr_resized,
4608 cv::Size(recording_width, recording_height), 0.0,
4609 0.0, cv::INTER_LINEAR);
4610 hdr_output_pixels = hdr_resized.ptr<std::uint16_t>();
4611 }
4612 }
4613
4614 if (writer.is_open()) {
4615 if (hdr_output_enabled) {
4616 if (hdr_output_pixels == nullptr) {
4617 throw std::runtime_error(
4618 "HDR Main10 recording did not receive an RGBA16 "
4619 "Vulkan readback");
4620 }
4621 if (request.has_pts) {
4622 writer.write_hdr_rgba16_at_pts(
4623 const_cast<std::uint16_t *>(hdr_output_pixels),
4624 static_cast<std::int64_t>(request.pts));
4625 } else {
4626 writer.write_hdr_rgba16(
4627 const_cast<std::uint16_t *>(hdr_output_pixels));
4628 }
4629 } else if (request.has_pts) {
4630 writer.write_at_pts(output_pixels,
4631 static_cast<std::int64_t>(request.pts));
4632 } else {
4633 writer.write(output_pixels);
4634 }
4635 }
4636 if (options.png_output) {
4639 output_pixels, recording_width, recording_height);
4641 }
4642 if (options.generate_interval > 0 &&
4643 (request.has_pts ? request.pts : output_frame_count) %
4644 static_cast<std::uint64_t>(options.generate_interval) ==
4645 0) {
4649 output_pixels, recording_width, recording_height);
4651 }
4653 emitHeadlessProgress(false);
4654
4655 if (options.duration > 0.0) {
4656 double output_duration = 0.0;
4657 if (request.has_pts) {
4658 output_duration =
4659 static_cast<double>(request.pts + 1) / recording_fps;
4660 } else if (writer.is_open()) {
4661 output_duration = writer.get_duration();
4662 } else {
4663 output_duration =
4664 static_cast<double>(output_frame_count) / recording_fps;
4665 }
4666 if (output_duration >= options.duration) {
4667 recording_complete = true;
4669 exit();
4670 }
4671 }
4672
4673 if (options.max_size_mb > 0.0 && writer.is_open()) {
4674 const double maximum_bytes = options.max_size_mb * 1024.0 * 1024.0;
4675 if (static_cast<double>(writer.get_bytes_written()) >=
4676 maximum_bytes) {
4677 std::cout << "acmxvk: maximum output size reached ("
4678 << options.max_size_mb << " MB)\n";
4679 recording_complete = true;
4680 exit();
4681 }
4682 }
4683 }
4684
4686 std::vector<std::uint16_t> &rgba, uint32_t width, uint32_t height) {
4687 if (!hdr_readback_logged) {
4688 std::cout
4689 << "acmxvk: HDR readback: normalized RGBA16 received from "
4690 "the final Vulkan HDR intermediate"
4692 ? "; feeding MXWrite's HEVC Main10 encoder\n"
4693 : "; converting to RGBA8 for snapshots/output\n");
4694 hdr_readback_logged = true;
4695 }
4696 std::vector<std::uint8_t> rgba8 =
4697 tone_map_hdr_rgba16(rgba, hdr_transfer_hlg);
4698 handleFrameReadback(rgba8, &rgba, width, height);
4699 }
4700 // 3D rendering, crossfades, pipelines, history, and frame uploads.
4702 if (!options.enable_3d || model_initialized) {
4703 return;
4704 }
4705
4706 try {
4707 input_model.enableExtendedFragmentUniforms();
4708 input_model.load(this, options.model_file, "", "", 1.0F);
4709 input_model.setShaders(
4710 this, model_vertex_shader_path(options).string(),
4713 input_model.setBackfaceCulling(false);
4714 model_initialized = true;
4715 model_3d_active = true;
4716 model_last_render_time = std::chrono::steady_clock::now();
4717 std::cout << "acmxvk: loaded 3D model: "
4718 << options.model_file << " ("
4719 << input_model.model().vertices().size()
4720 << " vertices, "
4721 << input_model.model().indexCount()
4722 << " indices; skybox camera centered; view rotation "
4723 << (model_auto_rotate ? "enabled" : "disabled")
4724 << ")\n";
4725 } catch (...) {
4726 if (getDevice() != VK_NULL_HANDLE) {
4727 vkDeviceWaitIdle(getDevice());
4728 input_model.cleanup(this);
4729 }
4730 throw;
4731 }
4732 }
4733
4735 if (!ensureRenderResources()) {
4736 throw std::runtime_error("MXVK failed to initialize render resources");
4737 }
4738
4739 const auto [source_width, source_height] = source_dimensions();
4740
4741 if (frame_sprite == nullptr) {
4742 frame_sprite = createSprite(source_width, source_height);
4743 }
4744 frame_sprite->enableExtendedUBO();
4745 frame_sprite->setCustomUniforms(custom_uniform_values);
4747 frame_sprite->enableSpectrumTexture(spectrumBinCount());
4748 }
4750 frame_sprite->enableSpectrumHistoryTexture(
4752 static_cast<std::uint32_t>(options.audio_buffers));
4753 }
4754 if (historyCacheEnabled()) {
4756 frame_sprite->enableHistoryTextureRgba16Float(
4757 source_width, source_height,
4758 static_cast<uint32_t>(options.texture_cache_size));
4759 } else {
4760 frame_sprite->enableHistoryTexture(
4761 source_width, source_height,
4762 static_cast<uint32_t>(options.texture_cache_size));
4763 }
4764 }
4765 const std::string initial_fragment =
4766 options.history_test
4770 : std::string{};
4772 frame_sprite->createEmptySpriteRgba16(
4773 source_width, source_height,
4774 sprite_vertex_shader_path(options).string(), initial_fragment);
4775 } else {
4776 frame_sprite->createEmptySprite(
4777 source_width, source_height,
4778 sprite_vertex_shader_path(options).string(), initial_fragment);
4779 }
4780
4781 if (options.human_background &&
4782 human_overlay_sprite == nullptr) {
4783 human_overlay_sprite = createSprite(1, 1);
4784 human_overlay_sprite->enableHistoryTexture(
4785 static_cast<std::uint32_t>(source_width),
4786 static_cast<std::uint32_t>(source_height), 1U);
4787 const cv::Mat transparent(source_height, source_width,
4788 CV_8UC4, cv::Scalar::all(0));
4789 human_overlay_sprite->updateHistoryTexture(
4790 transparent.ptr(), transparent.cols, transparent.rows,
4791 static_cast<int>(transparent.step));
4792 }
4793
4795
4797 initial_frame_pending = false;
4801 } else if (!readTrackedInputFrame()) {
4802 std::cerr << "acmxvk: capture did not provide an initial frame\n";
4803 } else {
4804 initial_frame_pending = true;
4805 }
4806
4808 if (!currentShader().empty()) {
4809 std::cout << "acmxvk: " << activeShaderRole() << ' '
4810 << (shader_index + 1) << '/' << shaders.size()
4811 << ": " << currentShader() << '\n';
4812 }
4813 }
4814
4816 previous_frame = std::chrono::steady_clock::now();
4819 shader_time = 0.0;
4820 frame_count = 0;
4821 }
4822
4824 if (options.cross_fade_duration <= 0.0 || frame_count == 0 ||
4825 getDevice() == VK_NULL_HANDLE) {
4826 crossfade_active = false;
4827 crossfade_alpha = 1.0F;
4829 return;
4830 }
4831
4832 try {
4833 std::vector<std::uint8_t> captured;
4834 std::uint32_t captured_width = 0;
4835 std::uint32_t captured_height = 0;
4836 captureSnapshotPixels(captured, captured_width,
4837 captured_height);
4838 const VkExtent2D extent = getRenderExtent();
4839 if (captured.empty() || captured_width == 0U ||
4840 captured_height == 0U || extent.width == 0U ||
4841 extent.height == 0U) {
4842 throw std::runtime_error(
4843 "the previous rendered frame is unavailable");
4844 }
4845
4846 cv::Mat captured_rgba(static_cast<int>(captured_height),
4847 static_cast<int>(captured_width),
4848 CV_8UC4, captured.data());
4849 cv::Mat previous_rgba;
4850 if (captured_width == extent.width &&
4851 captured_height == extent.height) {
4852 previous_rgba = captured_rgba;
4853 } else {
4854 const double captured_aspect =
4855 static_cast<double>(captured_width) / captured_height;
4856 const double target_aspect =
4857 static_cast<double>(extent.width) / extent.height;
4858 cv::Rect crop(0, 0, static_cast<int>(captured_width),
4859 static_cast<int>(captured_height));
4860 if (captured_aspect > target_aspect) {
4861 crop.width = std::max(
4862 1, static_cast<int>(std::lround(
4863 captured_height * target_aspect)));
4864 crop.x =
4865 (static_cast<int>(captured_width) - crop.width) / 2;
4866 } else if (captured_aspect < target_aspect) {
4867 crop.height = std::max(
4868 1, static_cast<int>(std::lround(
4869 captured_width / target_aspect)));
4870 crop.y = (static_cast<int>(captured_height) -
4871 crop.height) /
4872 2;
4873 }
4874 cv::resize(captured_rgba(crop), previous_rgba,
4875 cv::Size(static_cast<int>(extent.width),
4876 static_cast<int>(extent.height)),
4877 0.0, 0.0, cv::INTER_LINEAR);
4878 }
4879
4880 if (crossfade_previous_sprite == nullptr) {
4881 crossfade_previous_sprite = createSprite(1, 1);
4882 }
4884 crossfade_previous_sprite->enableHistoryTextureRgba16Float(
4885 extent.width, extent.height, 1U);
4886 } else {
4887 crossfade_previous_sprite->enableHistoryTexture(
4888 extent.width, extent.height, 1U);
4889 }
4891 const cv::Mat linear_previous =
4892 decode_hdr_transfer(previous_rgba, hdr_transfer_hlg);
4893 crossfade_previous_sprite->updateHistoryTextureRgba16(
4894 linear_previous.ptr<std::uint16_t>(),
4895 static_cast<int>(extent.width),
4896 static_cast<int>(extent.height),
4897 static_cast<int>(linear_previous.step));
4898 } else {
4899 crossfade_previous_sprite->updateHistoryTexture(
4900 previous_rgba.ptr(), static_cast<int>(extent.width),
4901 static_cast<int>(extent.height),
4902 static_cast<int>(previous_rgba.step));
4903 }
4904 crossfade_alpha = 0.0F;
4905 crossfade_active = true;
4906 crossfade_start_time = std::chrono::steady_clock::now();
4909 } catch (const std::exception &error) {
4910 crossfade_active = false;
4911 crossfade_alpha = 1.0F;
4913 std::cerr << "acmxvk: crossfade snapshot unavailable: "
4914 << error.what() << "; switching immediately\n";
4915 }
4916 }
4917
4918 void MainWindow::updateCrossfade(const std::chrono::steady_clock::time_point now) {
4919 if (!crossfade_active) {
4920 return;
4921 }
4922 double elapsed = 0.0;
4923 double video_timeline = 0.0;
4925 currentVideoTimeline(video_timeline)) {
4926 if (video_timeline < crossfade_start_video_timeline) {
4927 crossfade_start_video_timeline = video_timeline;
4928 }
4929 elapsed = video_timeline - crossfade_start_video_timeline;
4930 } else {
4931 elapsed = std::chrono::duration<double>(
4933 .count();
4934 }
4935 crossfade_alpha = static_cast<float>(std::clamp(
4936 elapsed / options.cross_fade_duration, 0.0, 1.0));
4937 if (crossfade_alpha >= 1.0F) {
4938 crossfade_active = false;
4941 }
4942 }
4943
4944 void MainWindow::cycleCrossfade(int direction) {
4945 const auto count =
4946 static_cast<std::ptrdiff_t>(CROSSFADE_NAMES.size());
4947 auto index =
4948 static_cast<std::ptrdiff_t>(crossfade_shader_index) + direction;
4949 index = (index % count + count) % count;
4950 crossfade_shader_index = static_cast<std::size_t>(index);
4951 std::cout << "acmxvk: crossfade shader: "
4953 << (crossfade_shader_index + 1) << '/'
4954 << CROSSFADE_NAMES.size() << ")\n";
4955 }
4956
4957 void MainWindow::adjustModelScale(float amount) {
4959 return;
4960 }
4961 model_scale = std::clamp(model_scale + amount, 0.05F, 20.0F);
4962 std::cout << "acmxvk: model scale " << model_scale << '\n';
4963 }
4964
4967 return;
4968 }
4969 std::uniform_int_distribution<std::size_t> distribution(
4970 0, CROSSFADE_NAMES.size() - 1);
4971 std::size_t next = distribution(autopilot_rng);
4972 if (CROSSFADE_NAMES.size() > 1 &&
4973 next == crossfade_shader_index) {
4974 next = (next + 1) % CROSSFADE_NAMES.size();
4975 }
4977 }
4978
4981 std::cout << "acmxvk: pause is available for video and graphic input\n";
4982 return;
4983 }
4986 std::cout << "acmxvk: input pause "
4987 << (input_paused ? "enabled" : "disabled") << '\n';
4988 }
4989
4992 std::cout << "acmxvk: freeze is available for video and graphic input\n";
4993 return;
4994 }
4997 previous_frame = std::chrono::steady_clock::now();
4998 std::cout << "acmxvk: rendering freeze "
4999 << (rendering_frozen ? "enabled" : "disabled") << '\n';
5000 }
5001
5002 void MainWindow::stepShaderTime(double amount) {
5003 shader_time += amount;
5004 std::cout << "acmxvk: shader time stepped to " << shader_time << '\n';
5005 }
5006
5007 void MainWindow::adjustTimeSpeed(double amount) {
5008 options.time_speed += amount;
5009 if (std::abs(options.time_speed) < 0.01) {
5010 options.time_speed = 0.0;
5011 }
5012 std::cout << "acmxvk: shader time speed " << options.time_speed << '\n';
5013 }
5014
5016 SDL_Window *window = getSDLWindow();
5017 if (window == nullptr) {
5018 return;
5019 }
5020 const bool fullscreen =
5021 (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
5022 if (!SDL_SetWindowFullscreen(window, !fullscreen)) {
5023 std::cerr << "acmxvk: unable to toggle fullscreen: "
5024 << SDL_GetError() << '\n';
5025 return;
5026 }
5027 std::cout << "acmxvk: fullscreen "
5028 << (!fullscreen ? "enabled" : "disabled") << '\n';
5029 }
5030
5032 if (options.autopilot_random_timeout > 0) {
5033 std::uniform_int_distribution<int> distribution(
5034 4, std::max(4, options.autopilot_random_timeout));
5036 } else {
5037 autopilot_interval_frames = options.autopilot_frames;
5038 }
5039 }
5040
5041 void MainWindow::logSelectedPlaylistNode(std::string_view action) const {
5042 if (playlist.empty()) {
5043 return;
5044 }
5045 std::cout << "acmxvk: " << action << " playlist node "
5046 << (playlist_index + 1) << '/' << playlist.size() << ": "
5047 << playlist[playlist_index].name << " ("
5048 << playlist[playlist_index].shaders.size()
5049 << " passes)\n";
5050 }
5051
5052 [[nodiscard]] std::uint64_t MainWindow::autopilotFrameAdvance() {
5053 double video_timeline = 0.0;
5054 std::uint64_t video_frame_index = 0U;
5055 if (!currentVideoTimeline(video_timeline, &video_frame_index)) {
5057 return 1U;
5058 }
5059
5061 video_frame_index < previous_autopilot_video_frame) {
5062 previous_autopilot_video_frame = video_frame_index;
5064 return 1U;
5065 }
5066
5067 const std::uint64_t advance =
5068 video_frame_index - previous_autopilot_video_frame;
5069 previous_autopilot_video_frame = video_frame_index;
5070 return advance;
5071 }
5072
5073 void MainWindow::toggleAutopilot(bool sequential) {
5074 if (!playlist_enabled) {
5075 std::cout << "acmxvk: "
5076 << (sequential ? "sequential autopilot" : "autopilot")
5077 << " requires playlist mode (press P first)\n";
5078 return;
5079 }
5080 if (playlist.empty()) {
5081 std::cout << "acmxvk: autopilot has no playlist entries\n";
5082 return;
5083 }
5084
5085 if (autopilot_enabled && autopilot_sequential == sequential) {
5086 autopilot_enabled = false;
5087 autopilot_sequential = false;
5088 std::cout << "acmxvk: autopilot disabled\n";
5089 return;
5090 }
5091
5092 autopilot_enabled = true;
5093 autopilot_sequential = sequential;
5096 if (options.autopilot_random_timeout <= 0 && options.autopilot_frames <= 0) {
5097 options.autopilot_frames = 300;
5098 }
5100 std::cout << "acmxvk: " << (sequential ? "sequential " : "random ")
5101 << "autopilot enabled (";
5102 if (options.autopilot_random_timeout > 0) {
5103 std::cout << "random interval 4-" << options.autopilot_random_timeout
5104 << ", current " << autopilot_interval_frames;
5105 } else {
5106 std::cout << "every " << autopilot_interval_frames << " frames";
5107 }
5108 std::cout << ")\n";
5109 }
5110
5112 const std::uint64_t frame_advance = autopilotFrameAdvance();
5114 playlist.empty() || autopilot_interval_frames <= 0) {
5115 return;
5116 }
5117 const std::uint64_t remaining = static_cast<std::uint64_t>(
5119 if (frame_advance < remaining) {
5120 autopilot_counter += static_cast<int>(frame_advance);
5121 return;
5122 }
5124
5127 if (autopilot_sequential && options.autopilot_random_timeout <= 0) {
5128 playlist_index = (playlist_index + 1) % playlist.size();
5129 } else {
5130 std::uniform_int_distribution<std::size_t> distribution(0,
5131 playlist.size() - 1);
5132 std::size_t next = distribution(autopilot_rng);
5133 if (playlist.size() > 1 && next == playlist_index) {
5134 next = (next + 1) % playlist.size();
5135 }
5136 playlist_index = next;
5137 }
5138
5141 if (options.autopilot_random_timeout > 0) {
5143 }
5144 logSelectedPlaylistNode("autopilot selected");
5145 }
5146
5147 void MainWindow::selectShader(int direction) {
5148 if (shader_locked || shaders.size() < 2 || frame_sprite == nullptr) {
5149 return;
5150 }
5151 const auto count = static_cast<std::ptrdiff_t>(shaders.size());
5153 auto index = static_cast<std::ptrdiff_t>(shader_index) + direction;
5154 index = (index % count + count) % count;
5155 shader_index = static_cast<std::size_t>(index);
5156
5160 std::cout << "acmxvk: " << activeShaderRole() << ' '
5161 << (shader_index + 1) << '/' << shaders.size() << ": "
5162 << currentShader() << '\n';
5163 }
5164
5166 if (shader_locked || playlist.empty()) {
5167 return;
5168 }
5169 const auto count = static_cast<std::ptrdiff_t>(playlist.size());
5171 auto index = static_cast<std::ptrdiff_t>(playlist_index) + direction;
5172 index = (index % count + count) % count;
5173 playlist_index = static_cast<std::size_t>(index);
5177 logSelectedPlaylistNode("selected");
5178 }
5179
5180 [[nodiscard]] std::vector<fs::path> MainWindow::activeShaderPipeline() const {
5181 std::vector<fs::path> pipeline;
5182 if (effects_enabled) {
5183 if (playlist_enabled && !playlist.empty()) {
5184 pipeline = playlist[playlist_index].shaders;
5185 } else if (multipass_enabled) {
5186 pipeline = configured_passes;
5187 }
5188 if (!currentShader().empty()) {
5189 pipeline.emplace_back(currentShader());
5190 }
5191 }
5192 if (options.flip_output) {
5193 pipeline.emplace_back(flip_shader_path(options));
5194 }
5195 if (crossfade_active) {
5196 pipeline.emplace_back(
5198 }
5199 if (pipeline.empty()) {
5200 pipeline.emplace_back(passthrough_shader_path(options));
5201 }
5202 if (options.human_background) {
5203 pipeline.emplace_back(human_composite_shader_path(options));
5204 }
5205 for (fs::path &shader : pipeline)
5206 shader = resolvedShaderPath(shader);
5207 return pipeline;
5208 }
5209
5210 [[nodiscard]] fs::path MainWindow::directModelFragmentShader() const {
5214 currentShader().empty()) {
5215 return {};
5216 }
5217
5218 const fs::path shader = resolvedShaderPath(currentShader());
5219 const mxvk::ShaderModuleInfo module_info =
5220 mxvk::inspect_spirv(mxvk::load_spv(shader.string()));
5221 if (module_info.stage != mxvk::ShaderStage::Fragment ||
5222 module_info.usesHistoryTexture ||
5223 module_info.usesSpectrumTexture ||
5224 module_info.usesSpectrumHistoryTexture) {
5225 return {};
5226 }
5227 return shader;
5228 }
5229
5231 if (getDevice() == VK_NULL_HANDLE) {
5232 return;
5233 }
5234 vkDeviceWaitIdle(getDevice());
5235 detachPostProcessingShader();
5236 post_process_sprites.clear();
5237 setPostProcessingPresentFragmentShader(
5240 : std::string{});
5241 frame_sprite->setEffectsEnabled(effects_enabled);
5242
5243 const fs::path direct_model_shader = directModelFragmentShader();
5245 model_3d_active && direct_model_shader.empty();
5246 setPostProcessingTextureConsumerEnabled(
5248 if (model_initialized) {
5249 input_model.setColorAttachmentFormat(
5252 ? getSwapchainFormat()
5253 : getSceneColorFormat());
5254 const fs::path desired_model_shader =
5255 direct_model_shader.empty()
5257 : direct_model_shader;
5258 if (desired_model_shader != model_effect_shader) {
5259 input_model.setShaders(
5260 this, model_vertex_shader_path(options).string(),
5261 desired_model_shader.string());
5262 model_effect_shader = desired_model_shader;
5263 }
5264 }
5265
5266 std::vector<fs::path> pipeline = activeShaderPipeline();
5267 if (!direct_model_shader.empty()) {
5268 const auto selected = std::find(
5269 pipeline.begin(), pipeline.end(), direct_model_shader);
5270 if (selected != pipeline.end()) {
5271 pipeline.erase(selected);
5272 }
5273 if (pipeline.empty()) {
5274 pipeline.emplace_back(passthrough_shader_path(options));
5275 }
5276 std::cout << "acmxvk: 3D texture effect: "
5277 << direct_model_shader.filename().string()
5278 << " [fragment, evaluated on model UVs]\n";
5279 } else if (model_3d_active && effects_enabled &&
5280 !currentShader().empty()) {
5281 std::cout << "acmxvk: 3D texture prepass: fragment/compute "
5282 "chain output mapped onto model UVs\n";
5283 }
5285 pipeline.insert(
5286 pipeline.begin(),
5288 pipeline.emplace_back(
5290 }
5291 if (pipeline.empty()) {
5292 return;
5293 }
5294
5295 std::vector<PostProcessingEffect> effects;
5296 effects.reserve(pipeline.size());
5298 std::numeric_limits<std::size_t>::max();
5299 for (std::size_t index = 0; index < pipeline.size(); ++index) {
5300 const fs::path &shader = pipeline[index];
5301 PostProcessingEffect effect{
5302 shader.string(), {1.0F, 1.0F, 1.0F, 0.0F}, false};
5303 if (crossfade_active &&
5307 effect.historySource = crossfade_previous_sprite;
5308 effect.params[0] = crossfade_alpha;
5309 } else if (options.human_background &&
5311 effect.historySource = human_overlay_sprite;
5312 } else if (historyCacheEnabled()) {
5313 effect.historySource = frame_sprite;
5314 }
5316 effect.spectrumBinCount = spectrumBinCount();
5317 }
5319 effect.spectrumHistoryLayerCount =
5320 static_cast<std::uint32_t>(options.audio_buffers);
5321 }
5322 effects.push_back(effect);
5323 }
5324 post_process_sprites = attachPostProcessingShaders(effects);
5325 for (mxvk::VK_Sprite *sprite : post_process_sprites) {
5326 sprite->enableExtendedUBO();
5327 sprite->setCustomUniforms(custom_uniform_values);
5329 sprite->enableSpectrumTexture(spectrumBinCount());
5330 }
5332 sprite->enableSpectrumHistoryTexture(
5334 static_cast<std::uint32_t>(options.audio_buffers));
5335 }
5336 }
5337
5338 std::cout << "acmxvk: Vulkan shader pipeline (" << pipeline.size() << " passes):\n";
5339 for (std::size_t index = 0; index < pipeline.size(); ++index) {
5340 const bool compute =
5341 index < post_process_effect_stages.size() &&
5342 post_process_effect_stages[index] ==
5343 mxvk::ShaderStage::Compute;
5344 std::cout << " " << (index + 1) << ": "
5345 << pipeline[index].filename().string() << " ["
5346 << (compute ? "compute" : "fragment") << "]\n";
5347 }
5348 }
5349
5351 if (!readInputFrame()) {
5352 return false;
5353 }
5357 } else if (source_kind == SourceKind::Camera) {
5359 }
5360 return true;
5361 }
5362
5363 [[nodiscard]] bool MainWindow::skipInputFrame() {
5365 return false;
5366 }
5367 bool skipped = false;
5368#ifdef MXVK_WITH_FFMPEG_CAPTURE
5369 if (using_ffmpeg_capture) {
5370 skipped = ffmpeg_capture.skip();
5371 } else
5372#endif
5373 {
5374 skipped = capture.grab();
5375 }
5376 if (skipped) {
5379 }
5380 return skipped;
5381 }
5382
5383 [[nodiscard]] bool MainWindow::handleCaptureEnd(bool discard) {
5385 return true;
5386 }
5387 if (!options.repeat) {
5388 setFrameReadbackEnabled(false);
5390 exit();
5391 return false;
5392 }
5393
5394#ifdef MXVK_WITH_FFMPEG_CAPTURE
5395 if (using_ffmpeg_capture && ffmpeg_capture.seek_start()) {
5397 const bool restarted =
5398 discard ? skipInputFrame() : readTrackedInputFrame();
5399 if (restarted) {
5400 if (!ffmpeg_seek_repeat_logged) {
5401 std::cout
5402 << "acmxvk: video repeat: in-place FFmpeg seek; "
5403 << (ffmpeg_capture.using_hardware_decode()
5404 ? "NVDEC decoder and CUDA device preserved\n"
5405 : "software decoder preserved\n");
5406 ffmpeg_seek_repeat_logged = true;
5407 }
5408 return true;
5409 }
5410 std::cerr << "acmxvk: in-place FFmpeg repeat did not produce a "
5411 "frame; reopening the input\n";
5412 }
5413#endif
5415 if (!openVideoCapture() ||
5416 !(discard ? skipInputFrame() : readTrackedInputFrame())) {
5417 throw std::runtime_error("unable to restart video input: " + options.input_file);
5418 }
5419 return true;
5420 }
5421
5422 [[nodiscard]] bool MainWindow::readClockedVideoFrame(double clock_seconds) {
5423 const double rate = outputFrameRate();
5424 if (!std::isfinite(rate) || rate <= 0.0) {
5425 return readTrackedInputFrame();
5426 }
5427
5428 std::uint64_t target_frame = static_cast<std::uint64_t>(
5429 std::floor(std::max(clock_seconds, 0.0) * rate));
5430 if (target_frame < decoded_video_frame_count) {
5431 const double next_frame_time =
5432 static_cast<double>(decoded_video_frame_count) / rate;
5433 const double wait_seconds = next_frame_time - clock_seconds;
5434 if (wait_seconds > 0.0) {
5435 std::this_thread::sleep_for(
5436 std::chrono::duration<double>(wait_seconds));
5437 }
5438 double updated_clock = 0.0;
5439 if (mediaClockSeconds(updated_clock)) {
5440 target_frame = static_cast<std::uint64_t>(
5441 std::floor(std::max(updated_clock, 0.0) * rate));
5442 }
5443 }
5444 if (target_frame < decoded_video_frame_count) {
5445 return true;
5446 }
5447
5448 const std::uint64_t frames_to_advance =
5449 target_frame - decoded_video_frame_count + 1;
5450 for (std::uint64_t frame = 0; frame < frames_to_advance; ++frame) {
5451 const bool discard = frame + 1 < frames_to_advance;
5452 bool advanced = discard ? skipInputFrame()
5454 if (!advanced) {
5455 advanced = handleCaptureEnd(discard);
5456 }
5457 if (!advanced) {
5458 return false;
5459 }
5460 }
5461
5462 source_frame_received = true;
5463 recording_frame_due = true;
5467 std::cout << "acmxvk: media-clock synchronization active; "
5468 "late video frames will be skipped and encoded "
5469 "with timeline PTS\n";
5471 }
5472 return true;
5473 }
5474
5476#ifdef MXVK_WITH_FFMPEG_CAPTURE
5477 if (ffmpeg_capture.is_open()) {
5478 ffmpeg_capture.close();
5479 }
5480 using_ffmpeg_capture = false;
5481#endif
5482 if (capture.is_open()) {
5483 capture.close();
5484 }
5485 }
5486
5487 [[nodiscard]] bool MainWindow::openVideoCapture() {
5489 video_source_fps = 0.0;
5490#ifdef MXVK_WITH_FFMPEG_CAPTURE
5491 if (ffmpeg_capture.open(options.input_file, options.cuda_device)) {
5492 using_ffmpeg_capture = true;
5493 video_source_fps = ffmpeg_capture.fps();
5494 std::cout << "acmxvk: video capture: FFmpeg ";
5495 if (ffmpeg_capture.using_hardware_decode()) {
5496 std::cout << "with CUDA/NVDEC";
5497 if (ffmpeg_capture.hardware_decode_device() >= 0) {
5498 std::cout << " on device "
5499 << ffmpeg_capture.hardware_decode_device();
5500 }
5501 std::cout << '\n';
5502 } else {
5503 std::cout << "software decode\n";
5504 }
5505 return true;
5506 }
5507#endif
5508 const bool opened = capture.open(options.input_file);
5509 if (opened) {
5510 video_source_fps = capture.get(cv::CAP_PROP_FPS);
5511 std::cout << "acmxvk: video capture: OpenCV fallback\n";
5512 }
5513 if (!std::isfinite(video_source_fps) || video_source_fps <= 0.0) {
5514 video_source_fps = 30.0;
5515 }
5516 return opened;
5517 }
5518
5519 [[nodiscard]] bool MainWindow::readHostRgba(cv::Mat &rgba) {
5520#ifdef MXVK_WITH_FFMPEG_CAPTURE
5521 if (using_ffmpeg_capture) {
5522 int width = 0;
5523 int height = 0;
5524 int pitch = 0;
5526 if (!ffmpeg_capture.readRgba16(ffmpeg_rgba16, width, height,
5527 pitch, false) ||
5528 ffmpeg_rgba16.empty() || width <= 0 || height <= 0 ||
5529 pitch < width * 8) {
5530 return false;
5531 }
5532 rgba = cv::Mat(height, width, CV_16UC4,
5533 ffmpeg_rgba16.data(),
5534 static_cast<std::size_t>(pitch));
5535 return true;
5536 }
5537 if (!ffmpeg_capture.readRgba(ffmpeg_rgba, width, height, pitch,
5538 false) ||
5539 ffmpeg_rgba.empty() || width <= 0 || height <= 0 ||
5540 pitch < width * 4) {
5541 return false;
5542 }
5543 rgba = cv::Mat(height, width, CV_8UC4, ffmpeg_rgba.data(),
5544 static_cast<std::size_t>(pitch));
5545 return true;
5546 }
5547#endif
5548 return capture.readRgba(rgba, false);
5549 }
5550
5551 void MainWindow::initializeHistory(const cv::Mat &rgba) {
5553 return;
5554 }
5555 for (uint32_t layer = 0; layer < frame_sprite->getHistoryLayerCount(); ++layer) {
5556 updateHistoryFrame(rgba);
5557 }
5558 history_initialized = true;
5561 std::cout << "acmxvk: initialized " << frame_sprite->getHistoryLayerCount()
5562 << " Vulkan history-cache layers (delay " << options.cache_delay
5563 << ")\n";
5564 }
5565
5566 void MainWindow::updateHistoryFrame(const cv::Mat &rgba) {
5567#ifdef ACMXVK_WITH_CUDA
5568 if (gpu_filter_engine != nullptr && rgba.type() == CV_8UC4) {
5569 updateFilteredCudaHistoryFrame();
5570 return;
5571 }
5572#endif
5573 if (rgba.type() == CV_16UC4) {
5575 const cv::Mat linear_history =
5576 decode_hdr_transfer(rgba, hdr_transfer_hlg);
5577 frame_sprite->updateHistoryTextureRgba16(
5578 linear_history.ptr<std::uint16_t>(), linear_history.cols,
5579 linear_history.rows,
5580 static_cast<int>(linear_history.step));
5581 return;
5582 }
5583 frame_sprite->updateHistoryTextureRgba16(
5584 rgba.ptr<uint16_t>(), rgba.cols, rgba.rows,
5585 static_cast<int>(rgba.step));
5586 return;
5587 }
5588 frame_sprite->updateHistoryTexture(rgba.ptr(), rgba.cols, rgba.rows,
5589 static_cast<int>(rgba.step));
5590 }
5591
5595 return;
5596 }
5597
5598 const double rate = outputFrameRate();
5599 if (!std::isfinite(rate) || rate <= 0.0) {
5600 return;
5601 }
5602 const auto interval = std::chrono::duration_cast<
5603 std::chrono::steady_clock::duration>(std::chrono::duration<double>(
5604 static_cast<double>(options.cache_delay + 1) / rate));
5605 const auto now = std::chrono::steady_clock::now();
5607 camera_history_next_update = now + interval;
5609 return;
5610 }
5611 if (now < camera_history_next_update) {
5612 return;
5613 }
5614
5615 bool history_updated = false;
5616#if defined(ACMXVK_WITH_CUDA) && defined(ACMXVK_WITH_DEEP_DREAM) && \
5617 defined(ACMXVK_WITH_MXVK_CUDA)
5618 if (options.gpu_filter_before_dream &&
5619 deep_dream_model != nullptr && gpu_filter_engine != nullptr &&
5620 !cuda_dream_rgba.empty()) {
5621 const cv::cuda::GpuMat &history_input =
5622 options.frame_rotation == FrameRotation::None
5623 ? cuda_dream_rgba
5624 : cuda_rotated_rgba;
5625 updateCudaHistoryFrame(history_input,
5626 gpu_filter_engine->stream());
5627 history_updated = true;
5628 }
5629#endif
5630#ifdef ACMXVK_WITH_CUDA
5631 if (!history_updated && gpu_filter_engine != nullptr) {
5632 updateFilteredCudaHistoryFrame();
5633 history_updated = true;
5634 }
5635#endif
5636 if (!history_updated && !latest_camera_history_rgba.empty()) {
5638 history_updated = true;
5639 }
5640 if (!history_updated) {
5641 return;
5642 }
5643
5644 camera_history_next_update += interval;
5645 if (camera_history_next_update <= now) {
5646 camera_history_next_update = now + interval;
5647 }
5648 }
5649
5650#ifdef ACMXVK_WITH_MXVK_CUDA
5651 void MainWindow::updateModelTextureCuda(const cv::cuda::GpuMat &rgba,
5652 cv::cuda::Stream &source_stream) {
5653 if (!model_initialized) {
5654 return;
5655 }
5656 if (input_model.updatePrimaryTextureCuda(rgba, source_stream)) {
5657 return;
5658 }
5659
5660 rgba.download(cuda_model_fallback_rgba, source_stream);
5661 source_stream.waitForCompletion();
5662 if (!cuda_model_fallback_logged) {
5663 std::cerr << "acmxvk: direct CUDA model-texture upload "
5664 "unavailable; using host staging\n";
5665 cuda_model_fallback_logged = true;
5666 }
5667 if (!input_model.updatePrimaryTexture(
5668 cuda_model_fallback_rgba.ptr(),
5669 cuda_model_fallback_rgba.cols,
5670 cuda_model_fallback_rgba.rows,
5671 static_cast<int>(cuda_model_fallback_rgba.step))) {
5672 throw std::runtime_error(
5673 "MXVK could not update the 3D model texture");
5674 }
5675 }
5676
5677 void MainWindow::updateCudaHistoryFrame(const cv::cuda::GpuMat &rgba,
5678 cv::cuda::Stream &source_stream) {
5679 if (frame_sprite->updateHistoryTextureCuda(rgba, source_stream)) {
5680 return;
5681 }
5682
5683 rgba.download(cuda_history_fallback_rgba, source_stream);
5684 source_stream.waitForCompletion();
5685 if (!cuda_history_fallback_logged) {
5686 std::cerr << "acmxvk: direct CUDA history upload unavailable; "
5687 "using a host-staging fallback\n";
5688 cuda_history_fallback_logged = true;
5689 }
5690 frame_sprite->updateHistoryTexture(
5691 cuda_history_fallback_rgba.ptr(),
5692 cuda_history_fallback_rgba.cols,
5693 cuda_history_fallback_rgba.rows,
5694 static_cast<int>(cuda_history_fallback_rgba.step));
5695 }
5696
5697#ifdef ACMXVK_WITH_CUDA
5698 void MainWindow::updateFilteredCudaHistoryFrame() {
5699 updateCudaHistoryFrame(gpu_filter_engine->output(),
5700 gpu_filter_engine->stream());
5701 }
5702#endif
5703
5704 void MainWindow::initializeCudaHistory(const cv::cuda::GpuMat &rgba,
5705 cv::cuda::Stream &source_stream,
5706 bool filtered) {
5707 if (!historyCacheEnabled() || history_initialized) {
5708 return;
5709 }
5710 for (uint32_t layer = 0;
5711 layer < frame_sprite->getHistoryLayerCount(); ++layer) {
5712 updateCudaHistoryFrame(rgba, source_stream);
5713 }
5714 history_initialized = true;
5715 history_delay_counter = 0;
5716 camera_history_clock_started = false;
5717 std::cout << "acmxvk: initialized "
5718 << frame_sprite->getHistoryLayerCount()
5719 << (filtered ? " filtered" : " NVDEC")
5720 << " Vulkan history-cache layers (delay "
5721 << options.cache_delay << ")\n";
5722 }
5723
5724#ifdef ACMXVK_WITH_CUDA
5725 void MainWindow::uploadInputFrame(const cv::cuda::GpuMat &rgba,
5726 cv::cuda::Stream &source_stream) {
5727 if (!gpu_filter_engine->process(rgba, source_stream)) {
5728 throw std::runtime_error(
5729 "acidcam-gpu rejected the CUDA RGBA input frame");
5730 }
5731 if (!frame_sprite->updateTextureCuda(
5732 gpu_filter_engine->output(),
5733 gpu_filter_engine->stream())) {
5734 throw std::runtime_error(
5735 "MXVK could not upload the CUDA-filtered frame");
5736 }
5737 updateModelTextureCuda(gpu_filter_engine->output(),
5738 gpu_filter_engine->stream());
5739 }
5740#endif
5741
5742 [[nodiscard]] const cv::cuda::GpuMat &
5743 MainWindow::rotateCudaFrame(const cv::cuda::GpuMat &rgba,
5744 cv::cuda::Stream &source_stream) {
5745 switch (options.frame_rotation) {
5747 return rgba;
5749 cv::cuda::transpose(rgba, cuda_rotation_transpose, source_stream);
5750 cv::cuda::flip(cuda_rotation_transpose, cuda_rotated_rgba, 1,
5751 source_stream);
5752 break;
5754 cv::cuda::flip(rgba, cuda_rotated_rgba, -1, source_stream);
5755 break;
5757 cv::cuda::transpose(rgba, cuda_rotation_transpose, source_stream);
5758 cv::cuda::flip(cuda_rotation_transpose, cuda_rotated_rgba, 0,
5759 source_stream);
5760 break;
5761 }
5762 return cuda_rotated_rgba;
5763 }
5764#endif
5765
5766 void MainWindow::uploadInputFrame(const cv::Mat &rgba) {
5767#ifdef ACMXVK_WITH_CUDA
5768 if (gpu_filter_engine != nullptr && rgba.type() == CV_8UC4) {
5769 if (!gpu_filter_engine->process(rgba)) {
5770 throw std::runtime_error(
5771 "acidcam-gpu rejected the RGBA input frame");
5772 }
5773 if (!frame_sprite->updateTextureCuda(
5774 gpu_filter_engine->output(),
5775 gpu_filter_engine->stream())) {
5776 throw std::runtime_error(
5777 "MXVK could not upload the CUDA-filtered frame");
5778 }
5779 updateModelTextureCuda(gpu_filter_engine->output(),
5780 gpu_filter_engine->stream());
5781 return;
5782 }
5783 if (gpu_filter_engine != nullptr && rgba.type() == CV_16UC4 &&
5785 std::cout
5786 << "acmxvk: HDR increment 2: bypassing RGBA8 CUDA filters to "
5787 "preserve the 16-bit source texture\n";
5789 }
5790#endif
5791 if (rgba.type() == CV_16UC4) {
5792 frame_sprite->updateTextureRgba16(
5793 rgba.ptr<std::uint16_t>(), rgba.cols, rgba.rows,
5794 static_cast<int>(rgba.step));
5796 std::cout << "acmxvk: uploaded first RGBA16 HDR source frame "
5797 "to Vulkan\n";
5799 }
5800 } else {
5801 frame_sprite->updateTexture(rgba.ptr(), rgba.cols, rgba.rows,
5802 static_cast<int>(rgba.step));
5803 }
5804
5805 cv::Mat model_compatible;
5806 const cv::Mat *model_input = &rgba;
5807 if (model_initialized && rgba.type() == CV_16UC4) {
5808 model_compatible = rgba16ToRgba8(rgba);
5809 model_input = &model_compatible;
5810 }
5811 if (model_initialized &&
5812 !input_model.updatePrimaryTexture(
5813 model_input->ptr(), model_input->cols, model_input->rows,
5814 static_cast<int>(model_input->step))) {
5815 throw std::runtime_error(
5816 "MXVK could not update the 3D model texture");
5817 }
5818 }
5819
5821 cv::Mat bgr;
5822 const bool wait_for_first = !async_camera_frame_uploaded &&
5825 if (!latest_camera_frame.takeLatest(bgr, wait_for_first)) {
5826 return false;
5827 }
5828
5829 cv::Mat rgba;
5830 cv::cvtColor(bgr, rgba, cv::COLOR_BGR2RGBA);
5831 applyDnnEffects(rgba);
5833 rotateFrame(rgba, options.frame_rotation);
5834 if (!human_overlay_rgba.empty()) {
5835 rotateFrame(human_overlay_rgba, options.frame_rotation);
5836 }
5837 uploadInputFrame(rgba);
5841
5842#ifdef ACMXVK_WITH_CUDA
5843 if (gpu_filter_engine != nullptr) {
5844 initializeCudaHistory(gpu_filter_engine->output(),
5845 gpu_filter_engine->stream(), true);
5846 return true;
5847 }
5848#endif
5849
5850 initializeHistory(rgba);
5851 return true;
5852 }
5853
5854#if defined(ACMXVK_WITH_MXVK_CUDA) && defined(ACMXVK_WITH_DEEP_DREAM)
5855 [[nodiscard]] bool MainWindow::readCudaDeepDreamFrame() {
5856 cv::cuda::Stream *capture_stream = nullptr;
5857#ifdef MXVK_WITH_FFMPEG_CAPTURE
5858 if (using_ffmpeg_capture) {
5859 if (!ffmpeg_capture.readGpuRgba(cuda_input_rgba,
5860 ffmpeg_cuda_stream, false)) {
5861 return false;
5862 }
5863 capture_stream = &ffmpeg_cuda_stream;
5864 } else
5865#endif
5866 {
5867 if (!capture.readGpuRgba(cuda_input_rgba, false)) {
5868 return false;
5869 }
5870 capture_stream = &capture.cudaStream();
5871 }
5872
5873 const cv::cuda::GpuMat *dream_input = &cuda_input_rgba;
5874 cv::cuda::Stream *dream_stream = capture_stream;
5875 bool filtered = false;
5876 const bool filter_before_dream = options.gpu_filter_before_dream;
5877#ifdef ACMXVK_WITH_CUDA
5878 if (filter_before_dream && gpu_filter_engine != nullptr) {
5879 if (!gpu_filter_engine->process(cuda_input_rgba,
5880 *capture_stream)) {
5881 throw std::runtime_error(
5882 "acidcam-gpu rejected the pre-dream CUDA frame");
5883 }
5884 dream_input = &gpu_filter_engine->output();
5885 dream_stream = &gpu_filter_engine->stream();
5886 filtered = true;
5887 }
5888#endif
5889
5890 updateRandomDreamSettings();
5891 dream::GradientAscentResult dream_result;
5892 const cv::cuda::GpuMat *processed_input = dream_input;
5893 bool dream_processed = false;
5894 try {
5895 dream_result = deep_dream_model->apply_gradient_ascent_cuda(
5896 *dream_input, cuda_dream_rgba, *dream_stream,
5897 dream::GradientAscentOptions{
5898 options.dream_iterations,
5899 static_cast<float>(options.dream_strength),
5900 static_cast<float>(options.dream_feedback),
5901 static_cast<float>(options.dream_zoom),
5902 static_cast<float>(options.dream_rotation),
5903 options.dream_size, options.dream_channel,
5904 options.dream_octaves,
5905 static_cast<float>(options.dream_octave_scale),
5906 options.dream_jitter, options.dream_smoothing});
5907 dream_processed =
5908 std::isfinite(dream_result.mean_pixel_change);
5909 if (dream_processed) {
5910 processed_input = &cuda_dream_rgba;
5911 } else {
5912 handleDeepDreamRuntimeError(
5913 "Deep Dream returned a non-finite CUDA frame");
5914 }
5915 } catch (const std::exception &error) {
5916 handleDeepDreamRuntimeError(error.what());
5917 }
5918
5919 const cv::cuda::GpuMat &render_input =
5920 rotateCudaFrame(*processed_input, *dream_stream);
5921 const cv::cuda::GpuMat *final_input = &render_input;
5922 cv::cuda::Stream *final_stream = dream_stream;
5923#ifdef ACMXVK_WITH_CUDA
5924 if (!filtered && gpu_filter_engine != nullptr) {
5925 if (!gpu_filter_engine->process(render_input, *dream_stream)) {
5926 throw std::runtime_error(
5927 "acidcam-gpu rejected the post-dream CUDA frame");
5928 }
5929 final_input = &gpu_filter_engine->output();
5930 final_stream = &gpu_filter_engine->stream();
5931 filtered = true;
5932 }
5933#endif
5934 if (!frame_sprite->updateTextureCuda(*final_input, *final_stream)) {
5935 final_input->download(cuda_input_fallback_rgba, *final_stream);
5936 final_stream->waitForCompletion();
5937 if (!cuda_input_fallback_logged) {
5938 std::cerr
5939 << "acmxvk: direct Deep Dream/Vulkan upload "
5940 "unavailable; using host staging\n";
5941 cuda_input_fallback_logged = true;
5942 }
5943 frame_sprite->updateTexture(
5944 cuda_input_fallback_rgba.ptr(), cuda_input_fallback_rgba.cols,
5945 cuda_input_fallback_rgba.rows,
5946 static_cast<int>(cuda_input_fallback_rgba.step));
5947 }
5948 updateModelTextureCuda(*final_input, *final_stream);
5949
5950 if (dream_processed && !dream_processing_logged) {
5951 std::cout << "acmxvk: Deep Dream CUDA working frame: "
5952 << dream_result.processed_width << 'x'
5953 << dream_result.processed_height << " -> "
5954 << render_input.cols << 'x' << render_input.rows
5955 << " Vulkan texture ("
5956 << dream_result.processed_octaves << " octave(s))\n";
5957 dream_processing_logged = true;
5958 }
5959 if (!cuda_input_path_logged) {
5960 std::cout
5961 << "acmxvk: CUDA interop path active: capture/NVDEC -> ";
5962 if (filter_before_dream && filtered) {
5963 std::cout << "acidcam-gpu -> ";
5964 }
5965 if (dream_processed) {
5966 std::cout << "LibTorch Deep Dream -> ";
5967 }
5968 if (options.frame_rotation != FrameRotation::None) {
5969 std::cout << "CUDA rotation -> ";
5970 }
5971 if (!filter_before_dream && filtered) {
5972 std::cout << "acidcam-gpu -> ";
5973 }
5974 std::cout << "Vulkan texture";
5975 if (cuda_input_fallback_logged) {
5976 std::cout << " (host-staging upload fallback)";
5977 }
5978 std::cout << '\n';
5979 cuda_input_path_logged = true;
5980 }
5981
5982 const bool history_was_initialized = history_initialized;
5983 initializeCudaHistory(*final_input, *final_stream, filtered);
5984 if (source_kind != SourceKind::Camera && history_was_initialized &&
5985 ++history_delay_counter > options.cache_delay) {
5986 updateCudaHistoryFrame(*final_input, *final_stream);
5987 history_delay_counter = 0;
5988 }
5989 if (source_kind == SourceKind::Camera) {
5990 camera_history_clock_started = false;
5991 }
5992 return true;
5993 }
5994#endif
5995
5996 [[nodiscard]] bool MainWindow::readInputFrame() {
5997 if (source_kind == SourceKind::Camera && options.maximize_fps) {
5998 return readLatestCameraFrame();
5999 }
6000#if defined(ACMXVK_WITH_MXVK_CUDA) && defined(ACMXVK_WITH_DEEP_DREAM)
6002#ifdef ACMXVK_WITH_DNN
6003 && edge_detector == nullptr && human_segmenter == nullptr &&
6004 generic_onnx_processor == nullptr
6005#endif
6006 ) {
6007 return readCudaDeepDreamFrame();
6008 }
6009#endif
6010#ifdef ACMXVK_WITH_CUDA
6011 if (gpu_filter_engine != nullptr && !hostPreprocessingEnabled() &&
6013 cv::cuda::Stream *capture_stream = nullptr;
6014#ifdef MXVK_WITH_FFMPEG_CAPTURE
6015 if (using_ffmpeg_capture) {
6016 if (!ffmpeg_capture.readGpuRgba(cuda_input_rgba,
6017 ffmpeg_cuda_stream, false)) {
6018 return false;
6019 }
6020 capture_stream = &ffmpeg_cuda_stream;
6021 } else
6022#endif
6023 {
6024 if (!capture.readGpuRgba(cuda_input_rgba, false)) {
6025 return false;
6026 }
6027 capture_stream = &capture.cudaStream();
6028 }
6029 const cv::cuda::GpuMat &filter_input =
6030 rotateCudaFrame(cuda_input_rgba, *capture_stream);
6031 uploadInputFrame(filter_input, *capture_stream);
6032 if (!cuda_input_path_logged) {
6033#ifdef MXVK_WITH_FFMPEG_CAPTURE
6034 if (using_ffmpeg_capture) {
6035 std::cout << "acmxvk: CUDA input path active: FFmpeg "
6036 << (ffmpeg_capture.using_hardware_decode()
6037 ? "NVDEC -> CUDA RGBA -> "
6038 : "software decode -> CUDA upload -> ");
6039 } else
6040#endif
6041 {
6042 std::cout
6043 << "acmxvk: CUDA input path active: MXVK capture -> ";
6044 }
6045 if (options.frame_rotation != FrameRotation::None) {
6046 std::cout << "CUDA rotation -> ";
6047 }
6048 std::cout
6049 << "acidcam-gpu temporal buffer -> Vulkan texture\n";
6050 cuda_input_path_logged = true;
6051 }
6052 const bool history_was_initialized = history_initialized;
6053 initializeCudaHistory(gpu_filter_engine->output(),
6054 gpu_filter_engine->stream(), true);
6056 history_was_initialized &&
6057 ++history_delay_counter > options.cache_delay) {
6058 updateFilteredCudaHistoryFrame();
6060 }
6061 return true;
6062 }
6063#endif
6064#ifdef ACMXVK_WITH_MXVK_CUDA
6065#if defined(MXVK_WITH_FFMPEG_CAPTURE)
6066 if (using_ffmpeg_capture &&
6067 ffmpeg_capture.using_hardware_decode() &&
6069 if (!ffmpeg_capture.readGpuRgba(cuda_input_rgba,
6070 ffmpeg_cuda_stream, false)) {
6071 return false;
6072 }
6073 const cv::cuda::GpuMat &render_input =
6074 rotateCudaFrame(cuda_input_rgba, ffmpeg_cuda_stream);
6075 if (!frame_sprite->updateTextureCuda(render_input,
6076 ffmpeg_cuda_stream)) {
6077 render_input.download(cuda_input_fallback_rgba,
6078 ffmpeg_cuda_stream);
6079 ffmpeg_cuda_stream.waitForCompletion();
6080 if (!cuda_input_fallback_logged) {
6081 std::cerr
6082 << "acmxvk: direct NVDEC/Vulkan upload unavailable; "
6083 "using host staging\n";
6084 cuda_input_fallback_logged = true;
6085 }
6086 frame_sprite->updateTexture(
6087 cuda_input_fallback_rgba.ptr(),
6088 cuda_input_fallback_rgba.cols,
6089 cuda_input_fallback_rgba.rows,
6090 static_cast<int>(cuda_input_fallback_rgba.step));
6091 }
6092 updateModelTextureCuda(render_input, ffmpeg_cuda_stream);
6093 if (!cuda_input_path_logged) {
6094 std::cout << "acmxvk: CUDA input path active: FFmpeg "
6095 "NVDEC -> CUDA RGBA -> ";
6096 if (options.frame_rotation != FrameRotation::None) {
6097 std::cout << "CUDA rotation -> ";
6098 }
6099 std::cout << "Vulkan texture";
6100 if (cuda_input_fallback_logged) {
6101 std::cout << " (host-staging fallback)";
6102 }
6103 std::cout << '\n';
6104 cuda_input_path_logged = true;
6105 }
6106 const bool history_was_initialized = history_initialized;
6107 initializeCudaHistory(render_input, ffmpeg_cuda_stream, false);
6108 if (history_was_initialized &&
6109 ++history_delay_counter > options.cache_delay) {
6110 updateCudaHistoryFrame(render_input, ffmpeg_cuda_stream);
6112 }
6113 return true;
6114 }
6115#endif
6116#endif
6117
6118 bool requires_host_frame = hdr_input_precision_enabled ||
6121 options.frame_rotation != FrameRotation::None ||
6123 if (!requires_host_frame
6124#ifdef MXVK_WITH_FFMPEG_CAPTURE
6125 && !using_ffmpeg_capture
6126#endif
6127 ) {
6128 return capture.readToSprite(*frame_sprite, false);
6129 }
6130
6131 cv::Mat rgba;
6132 if (!readHostRgba(rgba)) {
6133 return false;
6134 }
6135 applyDnnEffects(rgba);
6137 rotateFrame(rgba, options.frame_rotation);
6138 if (!human_overlay_rgba.empty()) {
6139 rotateFrame(human_overlay_rgba, options.frame_rotation);
6140 }
6141 uploadInputFrame(rgba);
6145 }
6146 const bool history_was_initialized = history_initialized;
6147 initializeHistory(rgba);
6148 if (source_kind != SourceKind::Camera && history_was_initialized &&
6149 ++history_delay_counter > options.cache_delay) {
6150 updateHistoryFrame(rgba);
6152 }
6153 return true;
6154 }
6155
6156 void MainWindow::updateShaderUniforms(int width, int height) {
6157 const auto now = std::chrono::steady_clock::now();
6158 updateCrossfade(now);
6159 const float wall_delta =
6160 std::chrono::duration<float>(now - previous_frame).count();
6161 previous_frame = now;
6162 ++frame_count;
6163
6164 double video_timeline = 0.0;
6165 const bool video_timeline_available =
6166 currentVideoTimeline(video_timeline);
6167 float delta = wall_delta;
6168 if (video_timeline_available) {
6170 std::cout
6171 << "acmxvk: shader clock: decoded video timeline; "
6172 "effects are independent of processing speed\n";
6174 }
6176 video_timeline < previous_video_shader_timeline) {
6178 video_timeline < previous_video_shader_timeline) {
6179 shader_time = 0.0;
6180 frame_count = 1;
6181 }
6182 delta = 0.0F;
6184 } else {
6185 delta = static_cast<float>(
6186 video_timeline - previous_video_shader_timeline);
6187 }
6188 previous_video_shader_timeline = video_timeline;
6189 } else if (options.normalized_time) {
6190 delta = static_cast<float>(1.0 / outputFrameRate());
6191 }
6192 const float frame_rate =
6193 video_timeline_available
6194 ? static_cast<float>(video_source_fps)
6195 : (delta > 0.0F ? 1.0F / delta : 0.0F);
6196 float raw_audio_amplitude = 0.0F;
6197 float audio_sensitivity = 1.0F;
6198 float audio_amplitude = 0.0F;
6199 float audio_frequency = 0.0F;
6200 float audio_peak = 0.0F;
6201 float audio_rms = 0.0F;
6202 float audio_smooth = 0.0F;
6203 float audio_low = 0.0F;
6204 float audio_mid = 0.0F;
6205 float audio_high = 0.0F;
6206 float audio_sample_rate = 44100.0F;
6207#ifdef AUDIO_ENABLED
6208 std::vector<float> spectrum_values;
6209 if (file_audio_source != nullptr && audio_engine != nullptr &&
6211 (file_audio_source->has_output_clock() ||
6213 double source_audio_time = 0.0;
6214 if (options.use_source_audio &&
6215 !file_audio_source->has_output_clock() &&
6216 mediaClockSeconds(source_audio_time)) {
6217 file_audio_source->process_at_time(
6218 source_audio_time, outputFrameRate(), *audio_engine);
6219 } else {
6220 file_audio_source->process_frame(outputFrameRate(),
6221 *audio_engine);
6222 }
6223 if (options.audio_trunc && !file_audio_source->is_active()) {
6224 std::cout << "acmxvk: audio source finished, stopping "
6225 "(--audio-trunc)\n";
6226 exit();
6227 }
6228 }
6229 if (audioSourceOpen()) {
6230 const audio::AudioMetrics metrics = audio_engine->metrics();
6231 const float warmup = updateAudioWarmup(now);
6232 raw_audio_amplitude = metrics.amplitude;
6233 audio_sensitivity = audio_engine->sensitivity();
6234 const float delta_scale = audio_delta_time ? delta : 1.0F;
6235 const float sense = audio_sensitivity * 4.0F * warmup;
6236 audio_amplitude = raw_audio_amplitude * audio_sensitivity *
6237 static_cast<float>(options.time_speed) *
6238 delta_scale * warmup;
6239 audio_frequency = metrics.frequency;
6240 audio_peak = std::sqrt(std::max(metrics.peak, 0.0F)) * sense;
6241 audio_rms = std::sqrt(std::max(metrics.rms, 0.0F)) * sense;
6242 audio_smooth = std::sqrt(std::max(metrics.smooth, 0.0F)) * sense;
6243 audio_low = std::sqrt(std::max(metrics.low, 0.0F)) * sense;
6244 audio_mid = std::sqrt(std::max(metrics.mid, 0.0F)) * sense;
6245 audio_high = std::sqrt(std::max(metrics.high, 0.0F)) * sense;
6246 audio_sample_rate = static_cast<float>(audio_engine->sample_rate());
6247 spectrum_values = audio_engine->spectrum();
6248 const float spectrum_scale =
6249 warmup *
6250 (spectrum_scale_by_sensitivity ? audio_sensitivity : 1.0F);
6251 for (float &value : spectrum_values) {
6252 value *= spectrum_scale;
6253 }
6254 }
6255#endif
6256 if (audio_time_active) {
6257 const float delta_scale = audio_delta_time ? delta : 1.0F;
6258 shader_time += static_cast<double>(raw_audio_amplitude) *
6259 static_cast<double>(audio_sensitivity) *
6260 options.time_speed *
6261 static_cast<double>(delta_scale);
6262 } else if (shader_time_active) {
6263 shader_time += static_cast<double>(delta) * options.time_speed;
6264 }
6265 if (!std::isfinite(shader_time)) {
6266 shader_time = 0.0;
6267 }
6269 audio_amplitude * raw_audio_amplitude;
6270 if (video_timeline_available) {
6271 const std::uint64_t source_frame =
6273 if (source_frame <= 58U) {
6274 legacy_alpha =
6275 0.2F + 0.1F * static_cast<float>(source_frame);
6276 } else {
6277 const std::uint64_t phase = (source_frame - 59U) % 100U;
6278 legacy_alpha =
6279 phase < 50U
6280 ? 5.9F - 0.1F * static_cast<float>(phase)
6281 : 1.1F +
6282 0.1F * static_cast<float>(phase - 50U);
6283 }
6284 } else if (legacy_alpha_increasing) {
6285 legacy_alpha += 0.1F;
6286 if (legacy_alpha >= 6.0F) {
6287 legacy_alpha = 6.0F;
6289 }
6290 } else {
6291 legacy_alpha -= 0.1F;
6292 if (legacy_alpha <= 1.0F) {
6293 legacy_alpha = 1.0F;
6295 }
6296 }
6297 const float elapsed = static_cast<float>(shader_time);
6298 const float compatibility_time = video_timeline_available
6299 ? static_cast<float>(
6300 video_timeline)
6301 : std::chrono::duration<float>(
6303 .count();
6304 const float shader_frame =
6305 video_timeline_available
6306 ? static_cast<float>(video_source_frame_count - 1U)
6307 : static_cast<float>(frame_count);
6308 frame_sprite->setShaderParams(1.0F, 1.0F, 1.0F, elapsed);
6309 frame_sprite->setMouseState(mouse_x, mouse_y, mouse_pressed ? 1.0F : 0.0F);
6310 frame_sprite->setUniform0(legacy_alpha, compatibility_time,
6311 static_cast<float>(width),
6312 static_cast<float>(height));
6313 frame_sprite->setUniform1(delta, audio_amplitude, audio_frequency,
6314 frame_rate);
6315 frame_sprite->setUniform2(shader_frame, elapsed,
6316 audio_sample_rate, audio_peak);
6317 frame_sprite->setUniform3(static_cast<float>(frame_sprite->getHistoryHead()),
6318 static_cast<float>(frame_sprite->getHistoryLayerCount()),
6319 audio_rms, audio_smooth);
6320 frame_sprite->setAudioBands(audio_low, audio_mid, audio_high);
6321
6323 model_fragment_uniforms.mouse = glm::vec4(
6324 mouse_x, mouse_y, mouse_pressed ? 1.0F : 0.0F, 0.0F);
6326 glm::vec4(legacy_alpha, compatibility_time,
6327 static_cast<float>(width),
6328 static_cast<float>(height));
6330 glm::vec4(delta, audio_amplitude, audio_frequency,
6331 frame_rate);
6332 model_fragment_uniforms.u2 = glm::vec4(
6333 shader_frame, elapsed, audio_sample_rate, audio_peak);
6334 model_fragment_uniforms.u3 = glm::vec4(
6335 static_cast<float>(frame_sprite->getHistoryHead()),
6336 static_cast<float>(frame_sprite->getHistoryLayerCount()),
6337 audio_rms, audio_smooth);
6338 for (std::size_t index = 0;
6339 index < custom_uniform_values.size() && index < 64U;
6340 ++index) {
6341 model_fragment_uniforms.custom_uniforms[index / 4U]
6342 [index % 4U] =
6343 custom_uniform_values[index];
6344 }
6345 model_fragment_uniforms.audio_bands =
6346 glm::vec4(audio_low, audio_mid, audio_high, 0.0F);
6347
6348 for (std::size_t index = 0; index < post_process_sprites.size(); ++index) {
6349 mxvk::VK_Sprite *sprite = post_process_sprites[index];
6350 if (crossfade_active &&
6352 setPostProcessingShaderParams(index, crossfade_alpha, 0.0F,
6353 0.0F, 0.0F);
6354 } else {
6355 setPostProcessingShaderParams(index, 1.0F, 1.0F, 1.0F,
6356 elapsed);
6357 }
6358 sprite->setMouseState(mouse_x, mouse_y, mouse_pressed ? 1.0F : 0.0F);
6359 sprite->setUniform0(legacy_alpha, compatibility_time,
6360 static_cast<float>(width),
6361 static_cast<float>(height));
6362 sprite->setUniform1(delta, audio_amplitude, audio_frequency,
6363 frame_rate);
6364 sprite->setUniform2(shader_frame, elapsed,
6365 audio_sample_rate, audio_peak);
6366 sprite->setUniform3(
6367 static_cast<float>(frame_sprite->getHistoryHead()),
6368 static_cast<float>(frame_sprite->getHistoryLayerCount()),
6369 audio_rms, audio_smooth);
6370 sprite->setAudioBands(audio_low, audio_mid, audio_high);
6371 }
6372#ifdef AUDIO_ENABLED
6373 if (!spectrum_values.empty()) {
6374 frame_sprite->updateSpectrumTexture(
6375 spectrum_values.data(),
6376 static_cast<std::uint32_t>(spectrum_values.size()));
6377 if (options.audio_buffers > 0) {
6378 frame_sprite->updateSpectrumHistoryTexture(
6379 spectrum_values.data(),
6380 static_cast<std::uint32_t>(spectrum_values.size()));
6381 }
6382 for (mxvk::VK_Sprite *sprite : post_process_sprites) {
6383 sprite->updateSpectrumTexture(
6384 spectrum_values.data(),
6385 static_cast<std::uint32_t>(spectrum_values.size()));
6386 if (options.audio_buffers > 0) {
6387 sprite->updateSpectrumHistoryTexture(
6388 spectrum_values.data(),
6389 static_cast<std::uint32_t>(spectrum_values.size()));
6390 }
6391 }
6392 }
6393#endif
6394 }
6395
6396} // namespace acmxvk
void transfer_audio(std::string_view, std::string_view)
Copy the audio track from one media file to another via FFmpeg.
std::chrono::steady_clock::duration source_playback_paused_duration
std::unordered_map< std::string, fs::path > shader_reload_overrides
std::unique_ptr< midi::MidiInput > midi_input
std::unique_ptr< audio::FileAudioSource > file_audio_source
InterfaceClient interface_client
void requestSnapshot(SnapshotFormat format)
mxvk::ModelFragmentUniforms model_fragment_uniforms
std::uint64_t previous_autopilot_video_frame
void updateCrossfade(const std::chrono::steady_clock::time_point now)
std::chrono::steady_clock::time_point hud_session_start
fs::path resolvedShaderPath(const fs::path &shader) const
std::uint64_t hud_fps_frame_count
void queueRuntimeHud(int &y, int line_height)
std::vector< mxvk::VK_Sprite * > post_process_sprites
std::uint64_t output_frame_count
void event(SDL_Event &event) override
void setSourcePlaybackClockPaused(bool paused)
std::chrono::steady_clock::time_point source_playback_clock_start
std::chrono::steady_clock::time_point source_playback_pause_start
mxvk::VK_Sprite * crossfade_previous_sprite
void adjustAudioSensitivity(float amount)
std::chrono::steady_clock::time_point camera_fps_last_tick
void cycleCrossfade(int direction)
void adjustModelScale(float amount)
void dispatchMidiAction(int action)
const std::vector< fs::path > * activePasses() const
bool mediaClockSeconds(double &seconds) const
std::vector< float > custom_uniform_values
void onSwapchainRecreated() override
std::chrono::steady_clock::time_point audio_warmup_last_tick
std::string activePassDescription() const
double hudVideoPositionSeconds() const
std::chrono::steady_clock::time_point window_title_last_update
std::deque< ReadbackRequest > readback_requests
MainWindow(Options options)
std::uint64_t generated_frame_count
void onRecordPostProcessingTexture(VkCommandBuffer command_buffer, std::uint32_t image_index, VkImageView texture_view, VkExtent2D texture_extent) override
void apply_interface_gpu_filter_state(const InterfaceGpuFilterState &requested, bool announce)
void apply_interface_audio_file_state(const InterfaceAudioFileState &requested)
std::vector< MidiKnobState > midi_knob_states
void apply_interface_shader_reload(const InterfaceReloadState &requested)
void updateWindowTitle(bool force=false)
bool continuousReadbackEnabled() const
void applyDnnEffects(cv::Mat &rgba)
std::array< int, 4 > midi_slider_uniform_indices
std::string_view activeShaderRole() const
float updateAudioWarmup(std::chrono::steady_clock::time_point now)
fs::path directModelFragmentShader() const
std::uint64_t recording_frame_pts
mxvk::VK_Sprite * frame_sprite
void handleFrameReadback(std::vector< std::uint8_t > &rgba, const std::vector< std::uint16_t > *rgba16, uint32_t width, uint32_t height)
std::chrono::steady_clock::time_point interface_next_connect_attempt
std::uint64_t observed_midi_drops
void onRecordCustomRendering(VkCommandBuffer command_buffer, std::uint32_t image_index) override
void onFrameReadbackRgba16(std::vector< std::uint16_t > &rgba, uint32_t width, uint32_t height) override
bool isMidiModelAction(int action) const
bool isMidiMappingSupported(const midi::MidiMapping &mapping) const
void selectGpuFilter(int direction)
void apply_interface_deep_dream_state(const InterfaceDeepDreamState &requested, bool announce)
bool spectrumHistoryEnabledForShaders() const
std::uint64_t next_clock_output_frame
std::chrono::steady_clock::time_point crossfade_start_time
static std::string formatHudTime(double seconds_value)
bool applyMidiCc(const midi::MidiMessage &message)
void apply_interface_overlay_state(const InterfaceOverlayState &requested, bool announce)
std::chrono::steady_clock::time_point previous_frame
void apply_interface_playback_state(const InterfacePlaybackState &requested, bool announce)
std::chrono::steady_clock::time_point hud_fps_last_tick
static bool usesMidiDeltaDirection(const midi::MidiMapping &mapping)
std::string hudVideoTimeString() const
bool readHostRgba(cv::Mat &rgba)
void toggleAutopilot(bool sequential)
std::uint32_t interface_last_reload_sequence
void recordModel(VkCommandBuffer command_buffer, std::uint32_t image_index, VkImageView texture_view)
void logSelectedPlaylistNode(std::string_view action) const
std::unique_ptr< audio::AudioEngine > audio_engine
std::vector< MidiCcMapping > midi_cc_mappings
mxvk::VK_Sprite * human_overlay_sprite
void apply_interface_uniform_values(const std::vector< InterfaceUniformValue > &uniform_values)
std::vector< fs::path > activeShaderPipeline() const
std::chrono::steady_clock::time_point next_render_tick
void stepShaderTime(double amount)
std::uint64_t previous_model_video_frame
void onFrameReadbackScheduled() override
bool hostPreprocessingEnabled() const
double hudWallElapsedSeconds() const
std::string activePlaylistDescription() const
void handleDeepDreamRuntimeError(std::string_view message)
SnapshotFormat pending_snapshot_format
std::string hudElapsedTimeString() const
std::pair< int, int > source_dimensions()
void apply_interface_multipass_state(const InterfaceMultipassState &requested)
std::uint64_t camera_fps_frame_count
static std::string captureFourccName(double value)
void initializeHistory(const cv::Mat &rgba)
std::uint32_t spectrumBinCount() const
void applyDeepDreamEffect(cv::Mat &rgba)
std::chrono::steady_clock::time_point camera_history_next_update
bool handleCaptureEnd(bool discard=false)
std::uint32_t interface_last_audio_file_sequence
void adjustTimeSpeed(double amount)
void uploadInputFrame(const cv::Mat &rgba)
SDL_Keycode midiActionKey(int action) const
bool readClockedVideoFrame(double clock_seconds)
bool currentVideoTimeline(double &timeline, std::uint64_t *frame_index=nullptr) const
void updateShaderUniforms(int width, int height)
LatestCameraFrame latest_camera_frame
std::chrono::steady_clock::time_point headless_progress_last_emit
std::vector< fs::path > configured_passes
bool applyMidiMap(const midi::MidiMessage &message)
bool spectrumTextureEnabledForShaders() const
std::vector< midi::MidiMapping > midi_action_mappings
void apply_interface_shader_selection(const std::string &requested_name)
bool isMidiSliderMapping(const midi::MidiMapping &mapping) const
void recordShaderResources(const mxvk::ShaderModuleInfo &module_info, std::string_view source)
std::vector< PlaylistNode > playlist
static std::string clipOverlayText(std::string text)
static constexpr std::uint32_t COMPATIBILITY_SPECTRUM_BIN_COUNT
bool setMidiUniform(std::size_t uniform_index, int value, std::string_view label)
std::uint64_t autopilotFrameAdvance()
mxvk::VKAbstractModel input_model
void emitHeadlessProgress(bool complete)
std::chrono::steady_clock::time_point compatibility_clock_start
std::vector< fs::path > shaders
void onFrameReadback(std::vector< std::uint8_t > &rgba, uint32_t width, uint32_t height) override
std::vector< ShaderManifest::CustomUniform > custom_uniforms
std::uint32_t interface_last_sequence
std::string_view midiActionName(int action) const
void updateHistoryFrame(const cv::Mat &rgba)
std::size_t crossfade_post_process_index
std::uint64_t video_source_frame_count
std::chrono::steady_clock::time_point model_last_render_time
std::string currentShader() const
SnapshotWriter snapshot_writer
void dispatchMidiModelAction(int action)
void selectShader(int direction)
std::uint64_t decoded_video_frame_count
std::uint64_t random_dream_period
void selectPlaylistNode(int direction)
static void savePng(const fs::path &path, std::uint8_t *rgba, int width, int height)
static std::string_view formatName(SnapshotFormat format) noexcept
static constexpr std::uint32_t spectrum_bin_count()
static bool mux_recording_into_video(std::vector< float > samples, unsigned int sample_rate, const std::string &video_path, double video_duration)
std::size_t selected_channels() const
static Model load(std::string_view filename, int cuda_device, std::string_view layer={}, bool use_half=false)
cv::Mat decode_hdr_transfer(const cv::Mat &rgba, bool hlg)
volatile std::sig_atomic_t HEADLESS_SHUTDOWN_REQUESTED
std::vector< std::uint8_t > tone_map_hdr_rgba16(const std::vector< std::uint16_t > &rgba, bool hlg)
bool write_wav_file(const AudioRecording &recording, const std::string &filename)
cv::Mat hardenedAlphaMask(const cv::Mat &image, const cv::Mat &mask, float black_point, float white_point)
Definition edge_dnn.cpp:836
cv::Mat isolateBody(const cv::Mat &image, const cv::Mat &mask, float black_point, float white_point)
Definition edge_dnn.cpp:847
void validate_file_size(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes)
void validate_spirv_file(const std::filesystem::path &path, std::string_view context)
std::string truncate_utf8(std::string_view value, std::size_t maximum_bytes, std::string_view suffix)
void validate_string(std::string_view value, StringKind kind, std::string_view context, bool allow_empty)
std::vector< MidiMapping > load_mapping_file(const std::string &filename)
Definition midi.cpp:53
std::vector< PlaylistNode > load_playlist(const fs::path &playlist_path, const std::vector< fs::path > &available_shaders, const fs::path &library_directory, std::ostream &warning_output)
fs::path resolveShaderManifestEntry(const fs::path &directory, std::string entry)
cv::Mat loadRgbaImage(const std::string &filename)
bool rotationSwapsDimensions(FrameRotation rotation)
fs::path default_model_path(const Options &options)
fs::path snapshot_path(const fs::path &directory, std::uint32_t width, std::uint32_t height, std::uint64_t &counter, SnapshotFormat format, std::chrono::system_clock::time_point timestamp)
std::size_t playlist_shader_count(const std::vector< PlaylistNode > &playlist) noexcept
fs::path echo_cache_shader_path(const Options &options)
double probeVideoDuration(const std::string &filename)
std::string trim(std::string text)
Definition options.cpp:1619
fs::path human_composite_shader_path(const Options &options)
ShaderManifest loadShaderManifest(const fs::path &directory)
fs::path model_fragment_shader_path(const Options &options)
void printVideoHdrInfo(const VideoHdrInfo &info, std::ostream &output)
fs::path output_frame_directory(const std::string &filename, std::string_view suffix)
fs::path model_vertex_shader_path(const Options &options)
void request_headless_shutdown(int signal_number) noexcept
int parseInteger(std::string_view text, std::string_view option)
Definition options.cpp:168
bool dimensions_supported(int width, int height)
Definition options.cpp:63
void rotateFrame(cv::Mat &frame, FrameRotation rotation)
fs::path passthrough_shader_path(const Options &options)
fs::path frame_path(const fs::path &directory, std::uint64_t index)
VideoHdrInfo probeVideoHdrInfo(const std::string &filename)
fs::path hdr_preview_shader_path(const Options &options, bool hlg)
fs::path find_shader_path(const std::vector< fs::path > &available_shaders, const fs::path &library_directory, std::string name)
fs::path crossfade_shader_path(const Options &options, std::size_t shader_index)
fs::path sprite_vertex_shader_path(const Options &options)
fs::path hdr_transfer_shader_path(const Options &options, bool hlg, bool encode)
double parseNumber(std::string_view text, std::string_view option)
Definition options.cpp:186
void create_output_directory(const fs::path &directory)
bool isValidCustomUniformName(const std::string &name)
constexpr std::array< std::string_view, 35 > CROSSFADE_NAMES
Definition options.hpp:15
fs::path overlay_font_path(const Options &options)
fs::path flip_shader_path(const Options &options)
fs::path find_resource(const Options &options, const fs::path &relative_path)
std::vector< std::string > shader_names
std::array< std::uint8_t, 3 > watermark_color
InterfaceOverlayState overlay
std::vector< InterfaceUniformValue > uniform_values
InterfaceAudioFileState audio_file
InterfaceMultipassState multipass
InterfaceGpuFilterState gpu_filters
InterfaceReloadState reload
InterfacePlaybackState playback
InterfaceDeepDreamState deep_dream
int audio_output_device
Definition options.hpp:58
std::string edge_model
Definition options.hpp:186
bool midi_device_specified
Definition options.hpp:119
double audio_recording_gain
Definition options.hpp:77
double dream_octave_scale
Definition options.hpp:84
std::string dream_layer
Definition options.hpp:190
bool use_source_audio
Definition options.hpp:90
double random_dream_interval
Definition options.hpp:85
bool gpu_filter_before_dream
Definition options.hpp:145
std::string dream_model
Definition options.hpp:189
double audio_pass_through_gain
Definition options.hpp:76
double dream_rotation
Definition options.hpp:83
std::string fragment_shader
Definition options.hpp:168
bool random_dream_specified
Definition options.hpp:142
std::string audio_file
Definition options.hpp:183
bool human_background
Definition options.hpp:146
std::string output_file
Definition options.hpp:176
std::string midi_map_file
Definition options.hpp:185
std::vector< int > gpu_filter_indices
Definition options.hpp:164
std::string watermark_text
Definition options.hpp:193
double dream_feedback
Definition options.hpp:81
std::vector< std::string > midi_cc_mappings
Definition options.hpp:163
double dream_strength
Definition options.hpp:80
int audio_input_device
Definition options.hpp:57
double audio_warm_rate
Definition options.hpp:75
std::string shader_directory
Definition options.hpp:167
std::string onnx_configuration
Definition options.hpp:188
std::string record_audio_file
Definition options.hpp:184
std::string human_model
Definition options.hpp:187
int gpu_frame_buffer_size
Definition options.hpp:61
double dream_zoom
Definition options.hpp:82
std::string input_file
Definition options.hpp:165
bool audio_pass_through
Definition options.hpp:113
bool enable_screenshot
Definition options.hpp:95
bool use_source_fps
Definition options.hpp:89
int generate_interval
Definition options.hpp:52
std::string compute_shader
Definition options.hpp:169
double audio_sensitivity
Definition options.hpp:74
std::vector< std::string > entries
std::vector< CustomUniform > custom_uniforms
std::vector< std::uint16_t > rgba16
std::vector< std::uint8_t > rgba
std::vector< float > samples
std::vector< LayerMetadata > layers
unsigned char status
Definition midi.hpp:21
unsigned char data1
Definition midi.hpp:22
unsigned char data2
Definition midi.hpp:23
std::vector< unsigned char > bytes
Definition midi.hpp:14