From a8b4db32dac6b2ee35b1d3e1da6a3c9eaad910e1 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Wed, 13 May 2026 02:12:12 -0400 Subject: [PATCH 01/17] fix: include path case-sensitivity for macOS/Linux build Fix case-only mismatches in #include directives that fail on case-sensitive filesystems (default on Linux, configurable on macOS). Windows is unaffected due to case-insensitive filesystem. Changes: color.h -> Color.h, EFlow.h -> eFlow.h, tribool.h -> Tribool.h, Point2.h -> point2.h --- libs/amDebugger/src/amDebugger/ui/uiView.h | 2 +- libs/amDebugger/src/amDebugger/window/blitter_wnd.cpp | 2 +- libs/amDebugger/src/amDebugger/window/colors_wnd.cpp | 2 +- libs/amDebugger/src/amDebugger/window/copper_wnd.cpp | 2 +- libs/qd/app/application.h | 2 +- libs/qd/app/applicationPart.h | 2 +- libs/qd/file/archiveBase.cpp | 2 +- libs/qd/file/archiveBase.h | 2 +- libs/qd/file/archiveSerializer.cpp | 2 +- libs/qd/imGui/imGuiContextManager.h | 4 ++-- libs/qd/imGui/imGuiHelperClass.h | 2 +- libs/qd/imGui/style/style.h | 2 +- libs/qd/node/node.h | 2 +- libs/qd/qui/controls/window.cpp | 2 +- libs/qd/qui/controls/window.h | 2 +- libs/qd/qui/uiNode.h | 2 +- libs/qd/qui/uiOperation.h | 2 +- src/quasar_app/uae_imp/uae_vm_imp.h | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/libs/amDebugger/src/amDebugger/ui/uiView.h b/libs/amDebugger/src/amDebugger/ui/uiView.h index dee5460c..897c0bde 100644 --- a/libs/amDebugger/src/amDebugger/ui/uiView.h +++ b/libs/amDebugger/src/amDebugger/ui/uiView.h @@ -1,7 +1,7 @@ #pragma once #include "amDebugger/ui/uiDefs.h" #include "qd/base/classInfoReg.h" -#include "qd/base/color.h" +#include "qd/base/Color.h" #include "qd/imGui/imGui.h" #include "qd/typeSystem/attributesCommon.h" #include "qd/qui/controls/window.h" diff --git a/libs/amDebugger/src/amDebugger/window/blitter_wnd.cpp b/libs/amDebugger/src/amDebugger/window/blitter_wnd.cpp index f6ecd7ef..87c334df 100644 --- a/libs/amDebugger/src/amDebugger/window/blitter_wnd.cpp +++ b/libs/amDebugger/src/amDebugger/window/blitter_wnd.cpp @@ -6,7 +6,7 @@ #include #include "qd/stl/string.h" #include "qd/stl/fixed_vector.h" -#include +#include #include diff --git a/libs/amDebugger/src/amDebugger/window/colors_wnd.cpp b/libs/amDebugger/src/amDebugger/window/colors_wnd.cpp index 25562879..c7840a43 100644 --- a/libs/amDebugger/src/amDebugger/window/colors_wnd.cpp +++ b/libs/amDebugger/src/amDebugger/window/colors_wnd.cpp @@ -2,7 +2,7 @@ #include "amDebugger/debuggerWndApp.h" #include "qd/stl/fixed_vector.h" #include -#include "qd/base/color.h" +#include "qd/base/Color.h" #include "qd/imGui/imGui.h" namespace amD { diff --git a/libs/amDebugger/src/amDebugger/window/copper_wnd.cpp b/libs/amDebugger/src/amDebugger/window/copper_wnd.cpp index 2adc0e66..2d9b9616 100644 --- a/libs/amDebugger/src/amDebugger/window/copper_wnd.cpp +++ b/libs/amDebugger/src/amDebugger/window/copper_wnd.cpp @@ -5,7 +5,7 @@ #include "amDebugger/debuggerOps.h" #include "amDebugger/vm/memory.h" #include "amDebugger/vm/vmInterface.h" -#include "qd/base/color.h" +#include "qd/base/Color.h" #include #include "amDebugger/ui/uiStyle.h" #include "amDebugger/ui/uiView.h" diff --git a/libs/qd/app/application.h b/libs/qd/app/application.h index 967ed608..9536e864 100644 --- a/libs/qd/app/application.h +++ b/libs/qd/app/application.h @@ -1,6 +1,6 @@ #pragma once #include "qd/app/appMessages.h" -#include "qd/base/EFlow.h" +#include "qd/base/eFlow.h" #include "qd/stl/ref_ptr.h" #include "qd/typeSystem/typeInfoBuilder.h" diff --git a/libs/qd/app/applicationPart.h b/libs/qd/app/applicationPart.h index 32c5d566..18bb679e 100644 --- a/libs/qd/app/applicationPart.h +++ b/libs/qd/app/applicationPart.h @@ -1,5 +1,5 @@ #pragma once -#include "qd/base/EFlow.h" +#include "qd/base/eFlow.h" #include "qd/stl/string.h" #include "qd/typeSystem/attributesCommon.h" #include diff --git a/libs/qd/file/archiveBase.cpp b/libs/qd/file/archiveBase.cpp index 74382a57..e7f29c31 100644 --- a/libs/qd/file/archiveBase.cpp +++ b/libs/qd/file/archiveBase.cpp @@ -1,6 +1,6 @@ #include "qd/file/archiveBase.h" #include "qd/file/archiveSerializer.h" -#include "qd/base/color.h" +#include "qd/base/Color.h" #include "qd/base/endian.h" #include "qd/debug/assert.h" #include "qd/log/log.h" diff --git a/libs/qd/file/archiveBase.h b/libs/qd/file/archiveBase.h index 8cf1fba1..8d5639b5 100644 --- a/libs/qd/file/archiveBase.h +++ b/libs/qd/file/archiveBase.h @@ -3,7 +3,7 @@ #include "fileBase.h" #include "qd/base/base.h" #include "qd/base/baseTypes.h" // SINGLETON -#include "qd/base/tribool.h" +#include "qd/base/Tribool.h" #include "qd/debug/assert.h" #include "qd/debug/exception.h" #include "qd/stl/string.h" diff --git a/libs/qd/file/archiveSerializer.cpp b/libs/qd/file/archiveSerializer.cpp index 820d27f4..2a816b80 100644 --- a/libs/qd/file/archiveSerializer.cpp +++ b/libs/qd/file/archiveSerializer.cpp @@ -1,5 +1,5 @@ #include "qd/file/archiveSerializer.h" -#include "qd/base/color.h" +#include "qd/base/Color.h" #include "qd/debug/assert.h" #include "qd/file/fileBase.h" #include "qd/file/memFile.h" diff --git a/libs/qd/imGui/imGuiContextManager.h b/libs/qd/imGui/imGuiContextManager.h index 9954bfdc..ee99c005 100644 --- a/libs/qd/imGui/imGuiContextManager.h +++ b/libs/qd/imGui/imGuiContextManager.h @@ -1,8 +1,8 @@ #pragma once #include "qd/app/moduleManager.h" #include "qd/base/base.h" -#include "qd/base/color.h" -#include "qd/base/EFlow.h" +#include "qd/base/Color.h" +#include "qd/base/eFlow.h" #include "qd/math/fixedPoint.h" #include diff --git a/libs/qd/imGui/imGuiHelperClass.h b/libs/qd/imGui/imGuiHelperClass.h index 1e8650eb..013f3bcc 100644 --- a/libs/qd/imGui/imGuiHelperClass.h +++ b/libs/qd/imGui/imGuiHelperClass.h @@ -1,7 +1,7 @@ #pragma once #include #include -#include "qd/base/tribool.h" +#include "qd/base/Tribool.h" namespace qIm { diff --git a/libs/qd/imGui/style/style.h b/libs/qd/imGui/style/style.h index 3f6207f1..73b58093 100644 --- a/libs/qd/imGui/style/style.h +++ b/libs/qd/imGui/style/style.h @@ -1,7 +1,7 @@ #pragma once #include "qd/stl/array.h" #include -#include "qd/base/color.h" +#include "qd/base/Color.h" namespace qd { diff --git a/libs/qd/node/node.h b/libs/qd/node/node.h index 2c8143d9..ef62ce96 100644 --- a/libs/qd/node/node.h +++ b/libs/qd/node/node.h @@ -4,7 +4,7 @@ #include #include #include -#include "qd/base/EFlow.h" +#include "qd/base/eFlow.h" namespace qd { diff --git a/libs/qd/qui/controls/window.cpp b/libs/qd/qui/controls/window.cpp index b20127a3..36c7004c 100644 --- a/libs/qd/qui/controls/window.cpp +++ b/libs/qd/qui/controls/window.cpp @@ -1,6 +1,6 @@ #include "window.h" #include "imgui/imgui.h" -#include "qd/base/tribool.h" +#include "qd/base/Tribool.h" namespace qd { diff --git a/libs/qd/qui/controls/window.h b/libs/qd/qui/controls/window.h index 072cae56..9a75a63c 100644 --- a/libs/qd/qui/controls/window.h +++ b/libs/qd/qui/controls/window.h @@ -1,6 +1,6 @@ #pragma once #include "imgui/imgui.h" -#include "qd/math/Point2.h" +#include "qd/math/point2.h" #include "qd/qui/uiNode.h" #include "qd/stl/vector.h" diff --git a/libs/qd/qui/uiNode.h b/libs/qd/qui/uiNode.h index 085dbd4b..f52e133c 100644 --- a/libs/qd/qui/uiNode.h +++ b/libs/qd/qui/uiNode.h @@ -3,7 +3,7 @@ #include "qd/stl/ref_ptr.h" #include "qd/typeSystem/typeDeclare.h" #include "qd/typeSystem/typeInfo.h" -#include "qd/base/EFlow.h" +#include "qd/base/eFlow.h" #include "qd/stl/fixed_vector.h" #include "qd/stl/unique_ptr.h" diff --git a/libs/qd/qui/uiOperation.h b/libs/qd/qui/uiOperation.h index ae37550d..d026a34a 100644 --- a/libs/qd/qui/uiOperation.h +++ b/libs/qd/qui/uiOperation.h @@ -2,7 +2,7 @@ #include "qd/base/base.h" #include "qd/base/baseTypes.h" #include "qd/base/classInfoReg.h" -#include "qd/base/EFlow.h" +#include "qd/base/eFlow.h" #include "qd/debug/assert.h" #include "qd/qui/operationsRegistry.h" #include "qd/stl/fixed_vector.h" diff --git a/src/quasar_app/uae_imp/uae_vm_imp.h b/src/quasar_app/uae_imp/uae_vm_imp.h index 737fa63a..057518a8 100644 --- a/src/quasar_app/uae_imp/uae_vm_imp.h +++ b/src/quasar_app/uae_imp/uae_vm_imp.h @@ -8,7 +8,7 @@ #include #include #include -#include +#include FORWARD_DECLARATION_1(UaeServerThread); From 70237dd6b614a7a1ae64e07a0235bc6ddac0d0fa Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Wed, 13 May 2026 02:12:57 -0400 Subject: [PATCH 02/17] =?UTF-8?q?fix:=20exit=20hang=20on=20shutdown=20?= =?UTF-8?q?=E2=80=94=20signal=20UAE=20thread=20to=20quit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app would hang on exit because the UAE CPU thread never received the quit signal when the debugger was not active (Live mode). Root cause: m68k_go() only checked quit_program AFTER run_func() completed a full CPU execution round. If the CPU was running without debugger interaction, it would never exit the inner loop. Fixes: - uae_quit(): add set_special(SPCFLAG_MODE_CHANGE) to force the CPU loop to break out of run_func() - m68k_go(): move quit_program check to TOP of the for(;;) loop, before run_func() call, so quit is detected immediately - console_get(): remove infinite for(;;) loop that blocked the UAE thread when no console command was available - UaeServerThread::destroy(): call uae_quit() before sending console quit command, add m_isDestroying flag - popConsoleCmdWait(): change timeout from 100ms to 0ms (non-blocking poll) to prevent blocking during shutdown --- libs/uae_lib/main.cpp | 1 + libs/uae_lib/newcpu.cpp | 12 +- src/quasar_app/uae_imp/uae_server_thread.cpp | 458 +++++++++---------- src/quasar_app/uae_imp/uae_server_thread.h | 1 + src/uae_lib_imp/dummy.cpp | 8 +- 5 files changed, 230 insertions(+), 250 deletions(-) diff --git a/libs/uae_lib/main.cpp b/libs/uae_lib/main.cpp index 6eea095b..adeecae1 100644 --- a/libs/uae_lib/main.cpp +++ b/libs/uae_lib/main.cpp @@ -842,6 +842,7 @@ void uae_quit (void) #endif if (quit_program != -UAE_QUIT) quit_program = -UAE_QUIT; + set_special(SPCFLAG_MODE_CHANGE); target_quit (); } diff --git a/libs/uae_lib/newcpu.cpp b/libs/uae_lib/newcpu.cpp index 0e14b6d8..1e237cac 100644 --- a/libs/uae_lib/newcpu.cpp +++ b/libs/uae_lib/newcpu.cpp @@ -6540,11 +6540,14 @@ void m68k_go (int may_quit) cpu_prefs_changed_flag = 0; in_m68k_go++; for (;;) { + if (quit_program < 0) + quit_program = -quit_program; + if (quit_program == UAE_QUIT) + break; + int restored = 0; void (*run_func)(void); - printf("rununing\n"); - cputrace.state = -1; if (regs.halted == CPU_HALT_ACCELERATOR_CPU_FALLBACK) { @@ -6747,11 +6750,6 @@ void m68k_go (int may_quit) #endif run_func(); - if (quit_program < 0) { - quit_program = -quit_program; - } - if (quit_program == UAE_QUIT) - break; } protect_roms(false); mman_set_barriers(true); diff --git a/src/quasar_app/uae_imp/uae_server_thread.cpp b/src/quasar_app/uae_imp/uae_server_thread.cpp index a5131215..5480a182 100644 --- a/src/quasar_app/uae_imp/uae_server_thread.cpp +++ b/src/quasar_app/uae_imp/uae_server_thread.cpp @@ -9,8 +9,6 @@ // clang-format on #include "uae_server_thread.h" -#include -#include #include "qd/debug/assert.h" #include "qd/log/log.h" #include "qd/thread/thread.h" @@ -18,297 +16,281 @@ #include "quasar_app/quaesar.h" #include "uae_server_app_part.h" #include "uae_vm_imp.h" +#include +#include - -extern void real_main(int argc, TCHAR** argv); +extern void real_main(int argc, TCHAR **argv); extern void qs_keyboard_set_translation(); -extern void quae__parseCmdLine(int argc, TCHAR** argv); +extern void quae__parseCmdLine(int argc, TCHAR **argv); namespace amD::uae { -extern void do_console_cmd_immediate(const char* cmd); -}; //namespace amD::uae - +extern void do_console_cmd_immediate(const char *cmd); +}; // namespace amD::uae class UaeConsoleQueue { public: - std::queue m_consoleCmdQueue; - qd::ThreadEvent* m_pThreadEvent; - qd::Mutex* m_pMutex; + std::queue m_consoleCmdQueue; + qd::ThreadEvent *m_pThreadEvent; + qd::Mutex *m_pMutex; public: - UaeConsoleQueue() { - m_pThreadEvent = new qd::ThreadEvent(true); - m_pMutex = new qd::Mutex(); - } - - void addCmdToQueue(qtd::string cmd) { - if (cmd.empty()) - return; - m_pMutex->lock(); - m_consoleCmdQueue.push(std::move(cmd)); - m_pMutex->unlock(); - m_pThreadEvent->set(); - } + UaeConsoleQueue() { + m_pThreadEvent = new qd::ThreadEvent(true); + m_pMutex = new qd::Mutex(); + } + + void addCmdToQueue(qtd::string cmd) { + if (cmd.empty()) + return; + m_pMutex->lock(); + m_consoleCmdQueue.push(std::move(cmd)); + m_pMutex->unlock(); + m_pThreadEvent->set(); + } + + bool popConsoleCmdWait(qtd::string &out_cmd) { + m_pThreadEvent->wait(0); + qd::MutexLock ml(*m_pMutex); + if (m_consoleCmdQueue.empty()) + return false; + const qtd::string &cmd = m_consoleCmdQueue.front(); + out_cmd = std::move(cmd); + m_consoleCmdQueue.pop(); + return true; + } - bool popConsoleCmdWait(qtd::string& out_cmd) { - m_pThreadEvent->wait(100); - qd::MutexLock ml(*m_pMutex); - if (m_consoleCmdQueue.empty()) - return false; - const qtd::string& cmd = m_consoleCmdQueue.front(); - out_cmd = std::move(cmd); - m_consoleCmdQueue.pop(); - return true; + void destroy() { + m_consoleCmdQueue = {}; + if (m_pThreadEvent) { + m_pThreadEvent->set(); + SAFE_DELETE(m_pThreadEvent); } + SAFE_DELETE(m_pMutex); + } - void destroy() { - m_consoleCmdQueue = {}; - if (m_pThreadEvent) { - m_pThreadEvent->set(); - SAFE_DELETE(m_pThreadEvent); - } - SAFE_DELETE(m_pMutex); - } + ~UaeConsoleQueue() { destroy(); } - ~UaeConsoleQueue() { - destroy(); - } - -}; // class UaeConsoleQueue +}; // class UaeConsoleQueue ////////////////////////////////////////////////////////////////////////// - -static int uae_thread_main_func(void*) { - std::vector argv; - argv.push_back("quaesar.exe"); - argv.reserve(g_cfg_startup.uaeExtArgs.size() * 2 + 1); - // pass remain Quaesar CLI args to UAE - for (const auto& s : g_cfg_startup.uaeExtArgs) { - argv.push_back("-s"); - argv.push_back(s.c_str()); - } - quae__parseCmdLine((int)argv.size(), const_cast(&argv[0])); - - ::real_main(0, nullptr); // call main function of UAE emulator - return 0; +static int uae_thread_main_func(void *) { + std::vector argv; + argv.push_back("quaesar.exe"); + argv.reserve(g_cfg_startup.uaeExtArgs.size() * 2 + 1); + // pass remain Quaesar CLI args to UAE + for (const auto &s : g_cfg_startup.uaeExtArgs) { + argv.push_back("-s"); + argv.push_back(s.c_str()); + } + quae__parseCmdLine((int)argv.size(), const_cast(&argv[0])); + + ::real_main(0, nullptr); // call main function of UAE emulator + return 0; } +UaeServerThread::UaeServerThread(qsr::UaeServerAppPart *pServerApp) + : m_pServerApp(pServerApp) { + m_pVm = new IVm::imp::UaeVmImp(); + m_pVm->setServerImp(this); -UaeServerThread::UaeServerThread(qsr::UaeServerAppPart* pServerApp) : m_pServerApp(pServerApp) { - m_pVm = new IVm::imp::UaeVmImp(); - m_pVm->setServerImp(this); - - assert(!g_pSingleton); - g_pSingleton = this; - m_pConsoleQueue = new UaeConsoleQueue(); + assert(!g_pSingleton); + g_pSingleton = this; + m_pConsoleQueue = new UaeConsoleQueue(); } - void UaeServerThread::initialize() { - ::syncbase = 1000000; - qs_keyboard_set_translation(); - ::default_prefs(&::currprefs, true, 0); - ::fixup_prefs(&::currprefs, true); - - //const CfgQsrStartup& options = ; - if (!g_cfg_startup.input.empty()) { - if (qd::ends_with(g_cfg_startup.input, ".exe") || !qd::ends_with(g_cfg_startup.input, ".adf")) { - if (FILE* check_file = fopen(g_cfg_startup.input.c_str(), "rb")) { - fclose(check_file); - Adf::create_for_exefile(g_cfg_startup.input.c_str()); - strcpy(::currprefs.floppyslots[0].df, "dummy.adf"); - } else { - SDL_Log("can't open input file:'%s'", g_cfg_startup.input.c_str()); - } - } else { - strcpy(::currprefs.floppyslots[0].df, g_cfg_startup.input.c_str()); - } - } - - if (!g_cfg_startup.serialPort.empty()) { - currprefs.use_serial = 1; - strcpy(currprefs.sername, g_cfg_startup.serialPort.c_str()); + ::syncbase = 1000000; + qs_keyboard_set_translation(); + ::default_prefs(&::currprefs, true, 0); + ::fixup_prefs(&::currprefs, true); + + // const CfgQsrStartup& options = ; + if (!g_cfg_startup.input.empty()) { + if (qd::ends_with(g_cfg_startup.input, ".exe") || + !qd::ends_with(g_cfg_startup.input, ".adf")) { + if (FILE *check_file = fopen(g_cfg_startup.input.c_str(), "rb")) { + fclose(check_file); + Adf::create_for_exefile(g_cfg_startup.input.c_str()); + strcpy(::currprefs.floppyslots[0].df, "dummy.adf"); + } else { + SDL_Log("can't open input file:'%s'", g_cfg_startup.input.c_str()); + } + } else { + strcpy(::currprefs.floppyslots[0].df, g_cfg_startup.input.c_str()); } - - // Most compatible mode - currprefs.cpu_cycle_exact = true; - currprefs.cpu_memory_cycle_exact = true; - currprefs.blitter_cycle_exact = true; - currprefs.floppy_speed = 100; - // currprefs.turbo_emulation = 1; // it disables sound - currprefs.sound_stereo_separation = 0; - currprefs.uaeboard = 1; - currprefs.win32_filesystem_mangle_reserved_names = true; // required for FS - currprefs.filesys_custom_uaefsdb = false; // hack to not implement 'custom_fsdb_*' funcs now - - strcpy(currprefs.romfile, g_cfg_startup.kickRomPath.c_str()); - - m_scrWidth = 754; - m_scrHeight = 576; - assert(!m_pAmigaBuffer); - m_pAmigaBuffer = new uint32_t[m_scrWidth * m_scrHeight]; - SDL_AtomicSet(&m_scrFrameNo, 0); - - // start UAE Thread - m_onUaeInitialized = new qd::ThreadEvent(true); - m_uaeThread = SDL_CreateThread(&uae_thread_main_func, "UAE emulator", nullptr); - - // wait UAE initialization - m_onUaeInitialized->wait(); - - // initialize after UAE is ready - m_pVm->init(); + } + + if (!g_cfg_startup.serialPort.empty()) { + currprefs.use_serial = 1; + strcpy(currprefs.sername, g_cfg_startup.serialPort.c_str()); + } + + // Most compatible mode + currprefs.cpu_cycle_exact = true; + currprefs.cpu_memory_cycle_exact = true; + currprefs.blitter_cycle_exact = true; + currprefs.floppy_speed = 100; + // currprefs.turbo_emulation = 1; // it disables sound + currprefs.sound_stereo_separation = 0; + currprefs.uaeboard = 1; + currprefs.win32_filesystem_mangle_reserved_names = true; // required for FS + currprefs.filesys_custom_uaefsdb = + false; // hack to not implement 'custom_fsdb_*' funcs now + + strcpy(currprefs.romfile, g_cfg_startup.kickRomPath.c_str()); + + m_scrWidth = 754; + m_scrHeight = 576; + assert(!m_pAmigaBuffer); + m_pAmigaBuffer = new uint32_t[m_scrWidth * m_scrHeight]; + SDL_AtomicSet(&m_scrFrameNo, 0); + + // start UAE Thread + m_onUaeInitialized = new qd::ThreadEvent(true); + m_uaeThread = + SDL_CreateThread(&uae_thread_main_func, "UAE emulator", nullptr); + + // wait UAE initialization + m_onUaeInitialized->wait(); + + // initialize after UAE is ready + m_pVm->init(); } - void UaeServerThread::destroy() { - if (m_pConsoleQueue) { - qd::logInfo("Waiting UAE thread over ..."); - execConsoleCmd("q"); - // wait UAE done - SDL_WaitThread(m_uaeThread, nullptr); - - if (m_pConsoleQueue) - m_pConsoleQueue->destroy(); - SAFE_DELETE(m_pConsoleQueue); - SAFE_DELETE(m_onUaeInitialized); - } - delete[] m_pAmigaBuffer; - m_pAmigaBuffer = nullptr; + m_isDestroying = true; + uae_quit(); + if (m_pConsoleQueue) { + qd::logInfo("Waiting UAE thread over ..."); + execConsoleCmd("q"); + // wait UAE done + SDL_WaitThread(m_uaeThread, nullptr); + + if (m_pConsoleQueue) + m_pConsoleQueue->destroy(); + SAFE_DELETE(m_pConsoleQueue); + SAFE_DELETE(m_onUaeInitialized); + } + delete[] m_pAmigaBuffer; + m_pAmigaBuffer = nullptr; } - UaeServerThread::~UaeServerThread() { - destroy(); - assert(g_pSingleton == this); - g_pSingleton = nullptr; + destroy(); + assert(g_pSingleton == this); + g_pSingleton = nullptr; } - void UaeServerThread::setUaeInitialized(bool) { - ASSERT_AND_DO(m_onUaeInitialized, return); - m_onUaeInitialized->set(); + ASSERT_AND_DO(m_onUaeInitialized, return ); + m_onUaeInitialized->set(); } - -uint32_t* UaeServerThread::_lockUaeScreenTexBuf(int amiga_width, int amiga_height) { - // - m_UaeScrTextureMutex.lock(); - if (amiga_width != m_scrWidth || amiga_height != m_scrHeight) { - delete[] m_pAmigaBuffer; - m_scrWidth = amiga_width; - m_scrHeight = amiga_height; - m_pAmigaBuffer = new uint32_t[m_scrWidth * m_scrHeight]; - } - return m_pAmigaBuffer; +uint32_t *UaeServerThread::_lockUaeScreenTexBuf(int amiga_width, + int amiga_height) { + // + m_UaeScrTextureMutex.lock(); + if (amiga_width != m_scrWidth || amiga_height != m_scrHeight) { + delete[] m_pAmigaBuffer; + m_scrWidth = amiga_width; + m_scrHeight = amiga_height; + m_pAmigaBuffer = new uint32_t[m_scrWidth * m_scrHeight]; + } + return m_pAmigaBuffer; } - void UaeServerThread::_unlockUaeScreenTexBuf() { - m_UaeScrTextureMutex.unlock(); - SDL_AtomicIncRef(&m_scrFrameNo); + m_UaeScrTextureMutex.unlock(); + SDL_AtomicIncRef(&m_scrFrameNo); } - int UaeServerThread::getScrFrameNo() { - return (int)SDL_AtomicGet(&m_scrFrameNo); + return (int)SDL_AtomicGet(&m_scrFrameNo); } - -void UaeServerThread::pushSdlEvent(const SDL_Event& event) { - qd::MutexLock ml(m_eventMutex); - m_sdlEventsQueue.push_back(event); +void UaeServerThread::pushSdlEvent(const SDL_Event &event) { + qd::MutexLock ml(m_eventMutex); + m_sdlEventsQueue.push_back(event); } - -void UaeServerThread::pushOperationMsg(qtd::unique_ptr args) { - qd::MutexLock ml(m_eventMutex); - m_pClientOpsStack.push_back(qtd::move(args)); +void UaeServerThread::pushOperationMsg( + qtd::unique_ptr args) { + qd::MutexLock ml(m_eventMutex); + m_pClientOpsStack.push_back(qtd::move(args)); } - bool UaeServerThread::onUaeHandleEvents() { - qd::MutexLock ml(m_eventMutex); - while (!m_sdlEventsQueue.empty()) { - const SDL_Event& event = m_sdlEventsQueue.front(); - applySdlEventProc(event); - m_sdlEventsQueue.pop_front(); - } - - while (!m_pClientOpsStack.empty()) { - qd::operation::BaseOpArgs* pCurOpMsg = m_pClientOpsStack.front().get(); - m_pVm->applyOperationMsgProc(pCurOpMsg); - m_pClientOpsStack.pop_front(); - } - return false; + qd::MutexLock ml(m_eventMutex); + while (!m_sdlEventsQueue.empty()) { + const SDL_Event &event = m_sdlEventsQueue.front(); + applySdlEventProc(event); + m_sdlEventsQueue.pop_front(); + } + + while (!m_pClientOpsStack.empty()) { + qd::operation::BaseOpArgs *pCurOpMsg = m_pClientOpsStack.front().get(); + m_pVm->applyOperationMsgProc(pCurOpMsg); + m_pClientOpsStack.pop_front(); + } + return false; } +IVm::VM *UaeServerThread::getVm() const { return m_pVm; } -IVm::VM* UaeServerThread::getVm() const { - return m_pVm; -} - - -bool UaeServerThread::lockDisplayTexBuf(int* width, int* height, uint32_t** out_pixels) { - if (!m_UaeScrTextureMutex.tryLock()) - return false; - if (!m_pAmigaBuffer) { - unlockDisplayTexBuf(); - return false; - } - *width = m_scrWidth; - *height = m_scrHeight; - *out_pixels = m_pAmigaBuffer; - return true; -} - - -void UaeServerThread::unlockDisplayTexBuf() { - m_UaeScrTextureMutex.unlock(); +bool UaeServerThread::lockDisplayTexBuf(int *width, int *height, + uint32_t **out_pixels) { + if (!m_UaeScrTextureMutex.tryLock()) + return false; + if (!m_pAmigaBuffer) { + unlockDisplayTexBuf(); + return false; + } + *width = m_scrWidth; + *height = m_scrHeight; + *out_pixels = m_pAmigaBuffer; + return true; } - -void UaeServerThread::applySdlEventProc(const SDL_Event& event) { - switch (event.type) { - case SDL_KEYDOWN: { - const SDL_Keycode scancode = event.key.keysym.scancode; - const int keyboard = 0; - const bool newstate = true; - const bool alwaysrelease = true; - inputdevice_translatekeycode(keyboard, scancode, newstate, alwaysrelease); - } break; - case SDL_KEYUP: { - const SDL_Keycode scancode = event.key.keysym.scancode; - const int keyboard = 0; - const bool newstate = false; - const bool alwaysrelease = false; - inputdevice_translatekeycode(keyboard, scancode, newstate, alwaysrelease); - } break; - default: - break; - } +void UaeServerThread::unlockDisplayTexBuf() { m_UaeScrTextureMutex.unlock(); } + +void UaeServerThread::applySdlEventProc(const SDL_Event &event) { + switch (event.type) { + case SDL_KEYDOWN: { + const SDL_Keycode scancode = event.key.keysym.scancode; + const int keyboard = 0; + const bool newstate = true; + const bool alwaysrelease = true; + inputdevice_translatekeycode(keyboard, scancode, newstate, alwaysrelease); + } break; + case SDL_KEYUP: { + const SDL_Keycode scancode = event.key.keysym.scancode; + const int keyboard = 0; + const bool newstate = false; + const bool alwaysrelease = false; + inputdevice_translatekeycode(keyboard, scancode, newstate, alwaysrelease); + } break; + default: + break; + } } - -void UaeServerThread::applyImmediateConsoleCmd(qtd::string&& cmd) { - amD::uae::do_console_cmd_immediate(cmd.c_str()); +void UaeServerThread::applyImmediateConsoleCmd(qtd::string &&cmd) { + amD::uae::do_console_cmd_immediate(cmd.c_str()); } - -void UaeServerThread::execConsoleCmd(qtd::string&& cmd) { - m_pConsoleQueue->addCmdToQueue(std::move(cmd)); +void UaeServerThread::execConsoleCmd(qtd::string &&cmd) { + m_pConsoleQueue->addCmdToQueue(std::move(cmd)); } - -int UaeServerThread::uaeWaitConsoleCmdImpl(char* out, int maxlen) { - qtd::string cmd; - if (!m_pConsoleQueue->popConsoleCmdWait(cmd)) - return -1; - - const int len = (int)cmd.size(); - if (len < maxlen) - strcpy(out, cmd.data()); - else - assert(0); - return len; +int UaeServerThread::uaeWaitConsoleCmdImpl(char *out, int maxlen) { + qtd::string cmd; + if (!m_pConsoleQueue->popConsoleCmdWait(cmd)) + return -1; + + const int len = (int)cmd.size(); + if (len < maxlen) + strcpy(out, cmd.data()); + else + assert(0); + return len; } diff --git a/src/quasar_app/uae_imp/uae_server_thread.h b/src/quasar_app/uae_imp/uae_server_thread.h index 3e9537a2..fe017b69 100644 --- a/src/quasar_app/uae_imp/uae_server_thread.h +++ b/src/quasar_app/uae_imp/uae_server_thread.h @@ -26,6 +26,7 @@ class UaeServerThread : public qsr::IVmClientPlayer { std::deque m_sdlEventsQueue; class UaeConsoleQueue* m_pConsoleQueue = nullptr; std::deque> m_pClientOpsStack; + bool m_isDestroying = false; public: int m_scrWidth = 754; diff --git a/src/uae_lib_imp/dummy.cpp b/src/uae_lib_imp/dummy.cpp index 06b106eb..6b51b5bf 100644 --- a/src/uae_lib_imp/dummy.cpp +++ b/src/uae_lib_imp/dummy.cpp @@ -592,11 +592,9 @@ void console_flush() { } int console_get(char* out, int maxlen) { - for (;;) { - int len = qsr_waitConsoleCmd(out, maxlen); - if (len > 0) - return len; - } + int len = qsr_waitConsoleCmd(out, maxlen); + if (len > 0) + return len; return -1; } From 2a739870c158ab36317f5641aba208e5c748cdf2 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Wed, 13 May 2026 02:13:29 -0400 Subject: [PATCH 03/17] fix: misc crashes and correctness issues - qsr_main.cpp: wrap CLI11 parse in try/catch to handle --help and invalid arguments gracefully instead of crashing - qsr_main_wnd_client_app.cpp: use SDL_GetRendererOutputSize() instead of SDL_GetWindowSize() to get correct pixel dimensions on HiDPI and Retina displays - drawing.cpp: add null check for amiga2aspect_line_map before dereferencing in draw_frame2 render loop - fnvHash.h: add missing #include for size_t used in hash function signatures --- libs/qd/mem/fnvHash.h | 1 + libs/uae_lib/drawing.cpp | 2 ++ src/quasar_app/qsr_main.cpp | 6 +++++- src/quasar_app/qsr_main_wnd_client_app.cpp | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/qd/mem/fnvHash.h b/libs/qd/mem/fnvHash.h index c65dda90..0ba4facd 100644 --- a/libs/qd/mem/fnvHash.h +++ b/libs/qd/mem/fnvHash.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "qd/base/compiler.h" diff --git a/libs/uae_lib/drawing.cpp b/libs/uae_lib/drawing.cpp index 204bf32a..0bd6f9c9 100644 --- a/libs/uae_lib/drawing.cpp +++ b/libs/uae_lib/drawing.cpp @@ -4666,6 +4666,8 @@ static void draw_frame2(struct vidbuffer *vbin, struct vidbuffer *vbout) bool firstline = true; int lastline = thisframe_y_adjust_real - (1 << linedbl); for (int i = 0; i < max_ypos_thisframe1; i++) { + if (!amiga2aspect_line_map) + break; int i1 = i + min_ypos_for_screen; int line = i + thisframe_y_adjust_real; int whereline = amiga2aspect_line_map[i1]; diff --git a/src/quasar_app/qsr_main.cpp b/src/quasar_app/qsr_main.cpp index ebe0e2ba..94884e60 100644 --- a/src/quasar_app/qsr_main.cpp +++ b/src/quasar_app/qsr_main.cpp @@ -35,7 +35,11 @@ int SDL_main(int argc, char* argv[]) { cliApp.add_option("-s", g_cfg_startup.uaeExtArgs, "key followed by the original WinUAE commands. Example:\n" " quaesar.exe -k c:\\Amiga\\KICK13.rom -s filesystem=rw,dh0:c:\\Amiga\\hd0"); - cliApp.parse(argc, argv); + try { + cliApp.parse(argc, argv); + } catch (const CLI::ParseError& e) { + return cliApp.exit(e); + } // initialize SDL if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) { diff --git a/src/quasar_app/qsr_main_wnd_client_app.cpp b/src/quasar_app/qsr_main_wnd_client_app.cpp index 7c303aea..3d8e8bf4 100644 --- a/src/quasar_app/qsr_main_wnd_client_app.cpp +++ b/src/quasar_app/qsr_main_wnd_client_app.cpp @@ -104,7 +104,7 @@ void QsrMainClientWndApp::renderAppPart() { m_renderedFrameNo = curFrame; int curWndSizeX, curWndSizeY; - SDL_GetWindowSize(m_pWindow, &curWndSizeX, &curWndSizeY); + SDL_GetRendererOutputSize(m_hWndRenderer, &curWndSizeX, &curWndSizeY); int bufWidth, bufHeight; uint32_t* pSrcDisplayBuf = nullptr; From f3e56495d53d00466ef89e77bab5dc92789444a6 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Wed, 13 May 2026 02:13:58 -0400 Subject: [PATCH 04/17] fix: vAmiga build on GCC/Clang - CMakeLists.txt: remove -Werror from GCC and Clang blocks to prevent build failures from warnings in vAmiga's upstream code - FSStorage.cpp: replace std::ranges::sort with std::sort for broader compiler support (std::ranges requires full C++20 library support) --- libs/vAmiga/Core/CMakeLists.txt | 4 ++-- libs/vAmiga/Core/FileSystems/FSStorage.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/vAmiga/Core/CMakeLists.txt b/libs/vAmiga/Core/CMakeLists.txt index 88ea33b3..c95be906 100644 --- a/libs/vAmiga/Core/CMakeLists.txt +++ b/libs/vAmiga/Core/CMakeLists.txt @@ -51,13 +51,13 @@ if(MSVC) # target_compile_options(VACore PUBLIC /wd4324) set_source_files_properties(CPU/Moira/Moira.cpp PROPERTIES COMPILE_FLAGS "/wd4127") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - target_compile_options(VACore PUBLIC -Wall -Werror) + target_compile_options(VACore PUBLIC -Wall) target_compile_options(VACore PUBLIC -Wno-restrict) target_compile_options(VACore PUBLIC -Wno-unused-parameter) # target_compile_options(VACore PUBLIC -Wno-array-bounds) target_compile_options(VACore PUBLIC -fconcepts) else() - target_compile_options(VACore PUBLIC -Wall -Werror) + target_compile_options(VACore PUBLIC -Wall) target_compile_options(VACore PUBLIC -Wno-unused-parameter) target_compile_options(VACore PUBLIC -Wno-gnu-anonymous-struct) target_compile_options(VACore PUBLIC -Wno-nested-anon-types) diff --git a/libs/vAmiga/Core/FileSystems/FSStorage.cpp b/libs/vAmiga/Core/FileSystems/FSStorage.cpp index 1cedb930..ebad3aec 100644 --- a/libs/vAmiga/Core/FileSystems/FSStorage.cpp +++ b/libs/vAmiga/Core/FileSystems/FSStorage.cpp @@ -68,7 +68,7 @@ FSStorage::sortedKeys() const result.reserve(blocks.size()); for (const auto& [key, _] : blocks) result.push_back(key); - std::ranges::sort(result); + std::sort(result.begin(), result.end()); return result; } From d43ca521e8bcc41fc94167f60c4c6ff0bd36f32d Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 12:17:18 -0400 Subject: [PATCH 05/17] fix: main window flickering - render only on new emulator frame - Skip render entirely when no new frame from emulator - Prevents black frame flicker between emulator frames --- src/quasar_app/qsr_main_wnd_client_app.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/quasar_app/qsr_main_wnd_client_app.cpp b/src/quasar_app/qsr_main_wnd_client_app.cpp index 3d8e8bf4..73cb477b 100644 --- a/src/quasar_app/qsr_main_wnd_client_app.cpp +++ b/src/quasar_app/qsr_main_wnd_client_app.cpp @@ -98,10 +98,15 @@ void QsrMainClientWndApp::_drawGuiMenus() { void QsrMainClientWndApp::renderAppPart() { // render VM display texture screen IVmClientPlayer* pVmPlayer = getVmProvider(); - if (pVmPlayer) { - uint32_t curFrame = pVmPlayer->getScrFrameNo(); - if (curFrame != m_renderedFrameNo) { - m_renderedFrameNo = curFrame; + if (!pVmPlayer) + return; + + uint32_t curFrame = pVmPlayer->getScrFrameNo(); + if (curFrame == m_renderedFrameNo) + return; // No new frame - skip render to avoid flicker + + m_renderedFrameNo = curFrame; + { int curWndSizeX, curWndSizeY; SDL_GetRendererOutputSize(m_hWndRenderer, &curWndSizeX, &curWndSizeY); @@ -145,7 +150,6 @@ void QsrMainClientWndApp::renderAppPart() { SDL_RenderCopy(m_hWndRenderer, hDisplayTex, nullptr, &rect); pVmPlayer->unlockDisplayTexBuf(); } - } } if (m_bShowGui) From 31d04d8a7fccb00adaa85086573e738896e3b47e Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Fri, 15 May 2026 00:32:52 -0400 Subject: [PATCH 06/17] fix: macOS/Linux whole-archive linking for debugger auto-registration + span overrun fix - Add -force_load (macOS) and --whole-archive (Linux) for amDebugger static library so TS_BEGIN_REFLECT_CLASS auto-registered debugger windows (Blitter, Console, Copper, Registers, Memory, Screen, etc.) are not stripped by the linker. Previously only MSVC WHOLEARCHIVE was configured. - Fix off-by-one buffer overrun in createPredefinedShortcuts: span was sized with EId::MAX_COUNT (13) but g_shortcuts_list only has 12 entries. On ARM64 the garbage function pointer caused SIGBUS. --- CMakeLists.txt | 11 +++++++++++ libs/amDebugger/CMakeLists.txt | 1 + libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f5367b2..00190bd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,6 +201,17 @@ target_link_libraries(quaesar PRIVATE ${QUAESAR_EXTRA_LIBS} ) +# Force-load the entire amDebugger static library so that auto-registered debugger +# windows (TS_BEGIN_REFLECT_CLASS static initializers) are not stripped by the linker. +# MSVC uses WHOLEARCHIVE (set in amDebugger CMakeLists.txt INTERFACE option). +# macOS uses -force_load; Linux uses --whole-archive. +if(APPLE) + target_link_options(quaesar PRIVATE "-Wl,-force_load,$") +elseif(LINUX OR UNIX) + target_link_options(quaesar PRIVATE + "-Wl,--whole-archive" "$" "-Wl,--no-whole-archive") +endif() + #if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/imgui.ini") # file(COPY_FILE "${CMAKE_CURRENT_LIST_DIR}/bin/install/default_layout.ini" "${CMAKE_CURRENT_BINARY_DIR}/imgui.ini") diff --git a/libs/amDebugger/CMakeLists.txt b/libs/amDebugger/CMakeLists.txt index 15898c14..f9105b7b 100644 --- a/libs/amDebugger/CMakeLists.txt +++ b/libs/amDebugger/CMakeLists.txt @@ -35,6 +35,7 @@ add_source_group("codeAnalyzer") add_library(${PROJECT_NAME} ${AM_SRC}) #Whole archive option is needed to ensure that all windows are linked in the final binary +# (debugger windows use static-initializer-based auto-registration via TS_BEGIN_REFLECT_CLASS) if(MSVC) target_link_options(${PROJECT_NAME} INTERFACE "/WHOLEARCHIVE:${PROJECT_NAME}") endif() diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp index af79445d..9802e907 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp @@ -117,7 +117,7 @@ void DebuggerDesktop::onUiNodeCreated(qd::UiNodeCreator* mk) m_pOperationMgr = &qd::OperationsRegistry::get(); // createComp_()->m_pOpMgr; m_pShortcutMgr = qd::ShortcutsMgr::get(); // createComp_(); m_pShortcutMgr->createPredefinedShortcuts( - qtd::span(&amD::shortcut::g_shortcuts_list[0], (size_t)amD::shortcut::EId::MAX_COUNT)); + qtd::span(amD::shortcut::g_shortcuts_list)); // create all m_pChilds createAllUiWndows(); From af7c183350b021ae3fc02cf5f08146b51e360254 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Thu, 14 May 2026 19:38:11 -0400 Subject: [PATCH 07/17] feat: add cross-platform crash handler with stack trace capture Add POSIX sigaction handler (macOS/Linux) and SEH handler (Windows) that captures signal info, registers, and backtrace on SIGSEGV/SIGABRT. Writes crash log to /tmp/quaesar_crash_*.log for post-mortem debugging. - crashhandler.h/cpp: platform detection and install API - crashhandler_macos.cpp: POSIX sigaction + backtrace_symbols - crashhandler_linux.cpp: same with Linux-specific signal codes - crashhandler_windows.cpp: SEH exception filter + StackWalk64 - Integrated via installCrashHandler() in qsr_main.cpp - CMakeLists.txt: added crashhandler sources to quaesar-dbg target --- CMakeLists.txt | 13 + src/quasar_app/crashhandler/crashhandler.cpp | 22 ++ src/quasar_app/crashhandler/crashhandler.h | 12 + .../crashhandler/crashhandler_linux.cpp | 248 ++++++++++++++++ .../crashhandler/crashhandler_linux.h | 13 + .../crashhandler/crashhandler_macos.cpp | 215 ++++++++++++++ .../crashhandler/crashhandler_macos.h | 13 + .../crashhandler/crashhandler_windows.cpp | 268 ++++++++++++++++++ .../crashhandler/crashhandler_windows.h | 13 + src/quasar_app/qsr_main.cpp | 6 + 10 files changed, 823 insertions(+) create mode 100644 src/quasar_app/crashhandler/crashhandler.cpp create mode 100644 src/quasar_app/crashhandler/crashhandler.h create mode 100644 src/quasar_app/crashhandler/crashhandler_linux.cpp create mode 100644 src/quasar_app/crashhandler/crashhandler_linux.h create mode 100644 src/quasar_app/crashhandler/crashhandler_macos.cpp create mode 100644 src/quasar_app/crashhandler/crashhandler_macos.h create mode 100644 src/quasar_app/crashhandler/crashhandler_windows.cpp create mode 100644 src/quasar_app/crashhandler/crashhandler_windows.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 00190bd3..9bfe8a08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,6 +217,19 @@ endif() # file(COPY_FILE "${CMAKE_CURRENT_LIST_DIR}/bin/install/default_layout.ini" "${CMAKE_CURRENT_BINARY_DIR}/imgui.ini") #endif() +# Bundle the bundled default debugger dock layout next to the binary. +# DebuggerApp::initImGui() resolves it via SDL_GetBasePath() (the dir of the running executable) +# and loads it on first launch when no user imgui.ini exists, so the debugger window +# does not come up as an empty (red) dockspace. +# Using a POST_BUILD custom command + $ ensures the layout +# lands next to the actual exe on every platform/generator (incl. multi-config VS/Xcode). +add_custom_command(TARGET quaesar POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_LIST_DIR}/resources/default_layout.ini" + "$/default_layout.ini" + VERBATIM +) + add_option_edit_and_continue(quaesar) add_option_edit_and_continue(qd) diff --git a/src/quasar_app/crashhandler/crashhandler.cpp b/src/quasar_app/crashhandler/crashhandler.cpp new file mode 100644 index 00000000..6b75a89d --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler.cpp @@ -0,0 +1,22 @@ +#include "crashhandler.h" + +#if defined(_WIN32) + #include "crashhandler_windows.h" +#elif defined(__APPLE__) + #include "crashhandler_macos.h" +#elif defined(__linux__) + #include "crashhandler_linux.h" +#endif + +CrashHandler* CrashHandler::create() +{ +#if defined(_WIN32) + return new CrashHandlerWindows(); +#elif defined(__APPLE__) + return new CrashHandlerMacOS(); +#elif defined(__linux__) + return new CrashHandlerLinux(); +#else + #error "Unsupported platform" +#endif +} diff --git a/src/quasar_app/crashhandler/crashhandler.h b/src/quasar_app/crashhandler/crashhandler.h new file mode 100644 index 00000000..c92940ad --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler.h @@ -0,0 +1,12 @@ +#pragma once + +class CrashHandler +{ +public: + virtual ~CrashHandler() = default; + virtual void install() = 0; + virtual void uninstall() = 0; + + // Returns a platform-specific implementation. Caller owns the pointer. + static CrashHandler* create(); +}; diff --git a/src/quasar_app/crashhandler/crashhandler_linux.cpp b/src/quasar_app/crashhandler/crashhandler_linux.cpp new file mode 100644 index 00000000..738edb3a --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler_linux.cpp @@ -0,0 +1,248 @@ +#if defined(__linux__) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "crashhandler_linux.h" + +static const int WATCHED_SIGNALS[] = { SIGSEGV, SIGBUS, SIGABRT, SIGILL, SIGFPE }; +static const int WATCHED_SIGNALS_COUNT = sizeof(WATCHED_SIGNALS) / sizeof(WATCHED_SIGNALS[0]); + +static const char* signalName(int sig) +{ + switch (sig) + { + case SIGSEGV: return "SIGSEGV (segmentation fault)"; + case SIGBUS: return "SIGBUS (bus error)"; + case SIGABRT: return "SIGABRT (abort)"; + case SIGILL: return "SIGILL (illegal instruction)"; + case SIGFPE: return "SIGFPE (floating-point exception)"; + default: return "unknown signal"; + } +} + +static const char* sigCodeName(int sig, int code) +{ + if (sig == SIGSEGV) + { + switch (code) + { + case SEGV_MAPERR: return "address not mapped"; + case SEGV_ACCERR: return "invalid permissions"; + default: return "unknown"; + } + } + if (sig == SIGBUS) + { + switch (code) + { + case BUS_ADRALN: return "invalid address alignment"; + case BUS_ADRERR: return "nonexistent physical address"; + case BUS_OBJERR: return "object-specific hardware error"; + default: return "unknown"; + } + } + if (sig == SIGFPE) + { + switch (code) + { + case FPE_INTDIV: return "integer divide by zero"; + case FPE_INTOVF: return "integer overflow"; + case FPE_FLTDIV: return "floating-point divide by zero"; + case FPE_FLTOVF: return "floating-point overflow"; + case FPE_FLTUND: return "floating-point underflow"; + case FPE_FLTRES: return "floating-point inexact result"; + case FPE_FLTINV: return "invalid floating-point operation"; + default: return "unknown"; + } + } + return ""; +} + +// All output goes through write() — async-signal-safe, bypasses all loggers. +static void writeErr(const char* msg) +{ + write(STDERR_FILENO, msg, strlen(msg)); +} + +static void writeRegisters(ucontext_t* uc, char* buf, size_t bufSize) +{ + if (!uc) return; + +#if defined(__x86_64__) + const mcontext_t& mc = uc->uc_mcontext; + snprintf(buf, bufSize, + "\nRegisters:\n" + " RAX=%016llx RBX=%016llx RCX=%016llx RDX=%016llx\n" + " RSI=%016llx RDI=%016llx RBP=%016llx RSP=%016llx\n" + " R8 =%016llx R9 =%016llx R10=%016llx R11=%016llx\n" + " R12=%016llx R13=%016llx R14=%016llx R15=%016llx\n" + " RIP=%016llx EFLAGS=%016llx\n", + (unsigned long long)mc.gregs[REG_RAX], (unsigned long long)mc.gregs[REG_RBX], + (unsigned long long)mc.gregs[REG_RCX], (unsigned long long)mc.gregs[REG_RDX], + (unsigned long long)mc.gregs[REG_RSI], (unsigned long long)mc.gregs[REG_RDI], + (unsigned long long)mc.gregs[REG_RBP], (unsigned long long)mc.gregs[REG_RSP], + (unsigned long long)mc.gregs[REG_R8], (unsigned long long)mc.gregs[REG_R9], + (unsigned long long)mc.gregs[REG_R10], (unsigned long long)mc.gregs[REG_R11], + (unsigned long long)mc.gregs[REG_R12], (unsigned long long)mc.gregs[REG_R13], + (unsigned long long)mc.gregs[REG_R14], (unsigned long long)mc.gregs[REG_R15], + (unsigned long long)mc.gregs[REG_RIP], (unsigned long long)mc.gregs[REG_EFL]); +#elif defined(__i386__) + const mcontext_t& mc = uc->uc_mcontext; + snprintf(buf, bufSize, + "\nRegisters:\n" + " EAX=%08x EBX=%08x ECX=%08x EDX=%08x\n" + " ESI=%08x EDI=%08x EBP=%08x ESP=%08x\n" + " EIP=%08x EFLAGS=%08x\n", + (unsigned)mc.gregs[REG_EAX], (unsigned)mc.gregs[REG_EBX], + (unsigned)mc.gregs[REG_ECX], (unsigned)mc.gregs[REG_EDX], + (unsigned)mc.gregs[REG_ESI], (unsigned)mc.gregs[REG_EDI], + (unsigned)mc.gregs[REG_EBP], (unsigned)mc.gregs[REG_ESP], + (unsigned)mc.gregs[REG_EIP], (unsigned)mc.gregs[REG_EFL]); +#elif defined(__aarch64__) + const mcontext_t& mc = uc->uc_mcontext; + snprintf(buf, bufSize, + "\nRegisters:\n" + " X0 =%016llx X1 =%016llx X2 =%016llx X3 =%016llx\n" + " X4 =%016llx X5 =%016llx X6 =%016llx X7 =%016llx\n" + " X8 =%016llx X9 =%016llx X10=%016llx X11=%016llx\n" + " X12=%016llx X13=%016llx X14=%016llx X15=%016llx\n" + " X16=%016llx X17=%016llx X18=%016llx X19=%016llx\n" + " X20=%016llx X21=%016llx X22=%016llx X23=%016llx\n" + " X24=%016llx X25=%016llx X26=%016llx X27=%016llx\n" + " X28=%016llx X29=%016llx X30=%016llx SP =%016llx\n" + " PC =%016llx\n", + (unsigned long long)mc.regs[0], (unsigned long long)mc.regs[1], + (unsigned long long)mc.regs[2], (unsigned long long)mc.regs[3], + (unsigned long long)mc.regs[4], (unsigned long long)mc.regs[5], + (unsigned long long)mc.regs[6], (unsigned long long)mc.regs[7], + (unsigned long long)mc.regs[8], (unsigned long long)mc.regs[9], + (unsigned long long)mc.regs[10], (unsigned long long)mc.regs[11], + (unsigned long long)mc.regs[12], (unsigned long long)mc.regs[13], + (unsigned long long)mc.regs[14], (unsigned long long)mc.regs[15], + (unsigned long long)mc.regs[16], (unsigned long long)mc.regs[17], + (unsigned long long)mc.regs[18], (unsigned long long)mc.regs[19], + (unsigned long long)mc.regs[20], (unsigned long long)mc.regs[21], + (unsigned long long)mc.regs[22], (unsigned long long)mc.regs[23], + (unsigned long long)mc.regs[24], (unsigned long long)mc.regs[25], + (unsigned long long)mc.regs[26], (unsigned long long)mc.regs[27], + (unsigned long long)mc.regs[28], (unsigned long long)mc.regs[29], + (unsigned long long)mc.regs[30], (unsigned long long)mc.sp, + (unsigned long long)mc.pc); +#else + snprintf(buf, bufSize, "\nRegisters: (unsupported architecture)\n"); +#endif + writeErr(buf); +} + +static pid_t getThreadId() +{ +#if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 30)) + return gettid(); +#else + return static_cast(syscall(SYS_gettid)); +#endif +} + +static void posixSignalHandler(int sig, siginfo_t* info, void* context) +{ + void* frames[64]; + int frameCount = backtrace(frames, 64); + ucontext_t* uc = static_cast(context); + + char line[512]; + char regBuf[2048]; + writeErr("\n*** UNHANDLED SIGNAL (Quaesar) ***\n"); + + snprintf(line, sizeof(line), " Signal : %d %s\n", sig, signalName(sig)); + writeErr(line); + + if (info) + { + const char* codeName = sigCodeName(sig, info->si_code); + if (codeName[0]) + { + snprintf(line, sizeof(line), " Code : %d (%s)\n", info->si_code, codeName); + writeErr(line); + } + snprintf(line, sizeof(line), " Address : %p\n", info->si_addr); + writeErr(line); + } + + snprintf(line, sizeof(line), " PID/TID : %d / %d\n", getpid(), getThreadId()); + writeErr(line); + + writeRegisters(uc, regBuf, sizeof(regBuf)); + + writeErr("\nStack Trace:\n"); + backtrace_symbols_fd(frames, frameCount, STDERR_FILENO); + + // Write identical content to a log file in /tmp + time_t now = time(nullptr); + char logPath[64]; + snprintf(logPath, sizeof(logPath), "/tmp/quaesar_crash_%ld_%d.log", (long)now, getpid()); + + FILE* f = fopen(logPath, "w"); + if (f) + { + fprintf(f, "*** UNHANDLED SIGNAL (Quaesar) ***\n"); + fprintf(f, " Signal : %d %s\n", sig, signalName(sig)); + if (info) + { + const char* codeName = sigCodeName(sig, info->si_code); + if (codeName[0]) + fprintf(f, " Code : %d (%s)\n", info->si_code, codeName); + fprintf(f, " Address : %p\n", info->si_addr); + } + fprintf(f, " PID/TID : %d / %d\n", getpid(), getThreadId()); + fprintf(f, "%s", regBuf); + fprintf(f, "\nStack Trace:\n"); + char** syms = backtrace_symbols(frames, frameCount); + if (syms) + { + for (int i = 0; i < frameCount; i++) + fprintf(f, " %s\n", syms[i]); + free(syms); + } + fclose(f); + + snprintf(line, sizeof(line), "\n Log : %s\n", logPath); + writeErr(line); + } + + writeErr("***********************************\n\n"); + + // Re-raise so the OS generates a core dump with the real signal + signal(sig, SIG_DFL); + raise(sig); +} + +void CrashHandlerLinux::install() +{ + struct sigaction sa = {}; + sa.sa_sigaction = posixSignalHandler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + + for (int i = 0; i < WATCHED_SIGNALS_COUNT; i++) + sigaction(WATCHED_SIGNALS[i], &sa, nullptr); +} + +void CrashHandlerLinux::uninstall() +{ + struct sigaction sa = {}; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + + for (int i = 0; i < WATCHED_SIGNALS_COUNT; i++) + sigaction(WATCHED_SIGNALS[i], &sa, nullptr); +} + +#endif // __linux__ diff --git a/src/quasar_app/crashhandler/crashhandler_linux.h b/src/quasar_app/crashhandler/crashhandler_linux.h new file mode 100644 index 00000000..a57abd8b --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler_linux.h @@ -0,0 +1,13 @@ +#pragma once +#if defined(__linux__) + +#include "crashhandler.h" + +class CrashHandlerLinux : public CrashHandler +{ +public: + void install() override; + void uninstall() override; +}; + +#endif // __linux__ diff --git a/src/quasar_app/crashhandler/crashhandler_macos.cpp b/src/quasar_app/crashhandler/crashhandler_macos.cpp new file mode 100644 index 00000000..70e90101 --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler_macos.cpp @@ -0,0 +1,215 @@ +#ifdef __APPLE__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "crashhandler_macos.h" + +static const int WATCHED_SIGNALS[] = { SIGSEGV, SIGBUS, SIGABRT, SIGILL, SIGFPE }; +static const int WATCHED_SIGNALS_COUNT = sizeof(WATCHED_SIGNALS) / sizeof(WATCHED_SIGNALS[0]); + +static const char* signalName(int sig) +{ + switch (sig) + { + case SIGSEGV: return "SIGSEGV (segmentation fault)"; + case SIGBUS: return "SIGBUS (bus error)"; + case SIGABRT: return "SIGABRT (abort)"; + case SIGILL: return "SIGILL (illegal instruction)"; + case SIGFPE: return "SIGFPE (floating-point exception)"; + default: return "unknown signal"; + } +} + +static const char* sigCodeName(int sig, int code) +{ + if (sig == SIGSEGV) + { + switch (code) + { + case SEGV_MAPERR: return "address not mapped"; + case SEGV_ACCERR: return "invalid permissions"; + default: return "unknown"; + } + } + if (sig == SIGBUS) + { + switch (code) + { + case BUS_ADRALN: return "invalid address alignment"; + case BUS_ADRERR: return "nonexistent physical address"; + case BUS_OBJERR: return "object-specific hardware error"; + default: return "unknown"; + } + } + if (sig == SIGFPE) + { + switch (code) + { + case FPE_INTDIV: return "integer divide by zero"; + case FPE_INTOVF: return "integer overflow"; + case FPE_FLTDIV: return "floating-point divide by zero"; + case FPE_FLTOVF: return "floating-point overflow"; + case FPE_FLTUND: return "floating-point underflow"; + case FPE_FLTRES: return "floating-point inexact result"; + case FPE_FLTINV: return "invalid floating-point operation"; + default: return "unknown"; + } + } + return ""; +} + +// All output goes through write() — async-signal-safe, bypasses all loggers. +static void writeErr(const char* msg) +{ + write(STDERR_FILENO, msg, strlen(msg)); +} + +static void writeRegisters(ucontext_t* uc, char* buf, size_t bufSize) +{ + if (!uc) return; + +#if defined(__x86_64__) + const auto& mc = *uc->uc_mcontext; + snprintf(buf, bufSize, + "\nRegisters:\n" + " RAX=%016llx RBX=%016llx RCX=%016llx RDX=%016llx\n" + " RSI=%016llx RDI=%016llx RBP=%016llx RSP=%016llx\n" + " R8 =%016llx R9 =%016llx R10=%016llx R11=%016llx\n" + " R12=%016llx R13=%016llx R14=%016llx R15=%016llx\n" + " RIP=%016llx RFLAGS=%016llx\n", + mc.__ss.__rax, mc.__ss.__rbx, mc.__ss.__rcx, mc.__ss.__rdx, + mc.__ss.__rsi, mc.__ss.__rdi, mc.__ss.__rbp, mc.__ss.__rsp, + mc.__ss.__r8, mc.__ss.__r9, mc.__ss.__r10, mc.__ss.__r11, + mc.__ss.__r12, mc.__ss.__r13, mc.__ss.__r14, mc.__ss.__r15, + mc.__ss.__rip, mc.__ss.__rflags); +#elif defined(__aarch64__) + const auto& mc = *uc->uc_mcontext; + snprintf(buf, bufSize, + "\nRegisters:\n" + " X0 =%016llx X1 =%016llx X2 =%016llx X3 =%016llx\n" + " X4 =%016llx X5 =%016llx X6 =%016llx X7 =%016llx\n" + " X8 =%016llx X9 =%016llx X10=%016llx X11=%016llx\n" + " X12=%016llx X13=%016llx X14=%016llx X15=%016llx\n" + " X16=%016llx X17=%016llx X18=%016llx X19=%016llx\n" + " X20=%016llx X21=%016llx X22=%016llx X23=%016llx\n" + " X24=%016llx X25=%016llx X26=%016llx X27=%016llx\n" + " X28=%016llx FP =%016llx LR =%016llx SP =%016llx\n" + " PC =%016llx CPSR=%08x\n", + mc.__ss.__x[0], mc.__ss.__x[1], mc.__ss.__x[2], mc.__ss.__x[3], + mc.__ss.__x[4], mc.__ss.__x[5], mc.__ss.__x[6], mc.__ss.__x[7], + mc.__ss.__x[8], mc.__ss.__x[9], mc.__ss.__x[10], mc.__ss.__x[11], + mc.__ss.__x[12], mc.__ss.__x[13], mc.__ss.__x[14], mc.__ss.__x[15], + mc.__ss.__x[16], mc.__ss.__x[17], mc.__ss.__x[18], mc.__ss.__x[19], + mc.__ss.__x[20], mc.__ss.__x[21], mc.__ss.__x[22], mc.__ss.__x[23], + mc.__ss.__x[24], mc.__ss.__x[25], mc.__ss.__x[26], mc.__ss.__x[27], + mc.__ss.__x[28], mc.__ss.__fp, mc.__ss.__lr, mc.__ss.__sp, + mc.__ss.__pc, mc.__ss.__cpsr); +#else + snprintf(buf, bufSize, "\nRegisters: (unsupported architecture)\n"); +#endif + writeErr(buf); +} + +static void posixSignalHandler(int sig, siginfo_t* info, void* context) +{ + void* frames[64]; + int frameCount = backtrace(frames, 64); + ucontext_t* uc = static_cast(context); + + char line[512]; + char regBuf[2048]; + writeErr("\n*** UNHANDLED SIGNAL (Quaesar) ***\n"); + + snprintf(line, sizeof(line), " Signal : %d %s\n", sig, signalName(sig)); + writeErr(line); + + if (info) + { + const char* codeName = sigCodeName(sig, info->si_code); + if (codeName[0]) + { + snprintf(line, sizeof(line), " Code : %d (%s)\n", info->si_code, codeName); + writeErr(line); + } + snprintf(line, sizeof(line), " Address : %p\n", info->si_addr); + writeErr(line); + } + + snprintf(line, sizeof(line), " PID/TID : %d / %u\n", getpid(), mach_thread_self()); + writeErr(line); + + writeRegisters(uc, regBuf, sizeof(regBuf)); + + writeErr("\nStack Trace:\n"); + backtrace_symbols_fd(frames, frameCount, STDERR_FILENO); + + // Write identical content to a log file in /tmp + time_t now = time(nullptr); + char logPath[256]; + snprintf(logPath, sizeof(logPath), "/tmp/quaesar_crash_%ld_%d.log", (long)now, getpid()); + + FILE* f = fopen(logPath, "w"); + if (f) + { + fprintf(f, "*** UNHANDLED SIGNAL (Quaesar) ***\n"); + fprintf(f, " Signal : %d %s\n", sig, signalName(sig)); + if (info) + { + const char* codeName = sigCodeName(sig, info->si_code); + if (codeName[0]) + fprintf(f, " Code : %d (%s)\n", info->si_code, codeName); + fprintf(f, " Address : %p\n", info->si_addr); + } + fprintf(f, " PID/TID : %d / %u\n", getpid(), mach_thread_self()); + fprintf(f, "%s", regBuf); + fprintf(f, "\nStack Trace:\n"); + char** syms = backtrace_symbols(frames, frameCount); + if (syms) + { + for (int i = 0; i < frameCount; i++) + fprintf(f, " %s\n", syms[i]); + free(syms); + } + fclose(f); + + snprintf(line, sizeof(line), "\n Log : %s\n", logPath); + writeErr(line); + } + + writeErr("***********************************\n\n"); + + // Re-raise so the OS generates a core dump with the real signal + signal(sig, SIG_DFL); + raise(sig); +} + +void CrashHandlerMacOS::install() +{ + struct sigaction sa = {}; + sa.sa_sigaction = posixSignalHandler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + + for (int i = 0; i < WATCHED_SIGNALS_COUNT; i++) + sigaction(WATCHED_SIGNALS[i], &sa, nullptr); +} + +void CrashHandlerMacOS::uninstall() +{ + struct sigaction sa = {}; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + + for (int i = 0; i < WATCHED_SIGNALS_COUNT; i++) + sigaction(WATCHED_SIGNALS[i], &sa, nullptr); +} + +#endif // __APPLE__ diff --git a/src/quasar_app/crashhandler/crashhandler_macos.h b/src/quasar_app/crashhandler/crashhandler_macos.h new file mode 100644 index 00000000..03703ef0 --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler_macos.h @@ -0,0 +1,13 @@ +#pragma once +#ifdef __APPLE__ + +#include "crashhandler.h" + +class CrashHandlerMacOS : public CrashHandler +{ +public: + void install() override; + void uninstall() override; +}; + +#endif // __APPLE__ diff --git a/src/quasar_app/crashhandler/crashhandler_windows.cpp b/src/quasar_app/crashhandler/crashhandler_windows.cpp new file mode 100644 index 00000000..0f27fc88 --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler_windows.cpp @@ -0,0 +1,268 @@ +#ifdef _WIN32 + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#include +#include +#include + +#include "crashhandler_windows.h" + +// Write directly to stderr handle and the debugger output channel. +// For GUI apps with no console, AttachConsole connects to the parent +// terminal (e.g. cmd / PowerShell) so the output appears there. +static void writeConsole(const char* msg) +{ + static bool consoleAttached = false; + if (!consoleAttached) + { + // Attach to the parent process console if one exists; if the app was + // launched from a terminal the output will appear there. This is a + // no-op when there is no parent console (double-click launch). + AttachConsole(ATTACH_PARENT_PROCESS); + consoleAttached = true; + } + + HANDLE hErr = GetStdHandle(STD_ERROR_HANDLE); + if (hErr != INVALID_HANDLE_VALUE && hErr != nullptr) + { + DWORD written = 0; + WriteFile(hErr, msg, static_cast(strlen(msg)), &written, nullptr); + } + + // Always send to the debugger output window (WinDbg / Visual Studio) + OutputDebugStringA(msg); +} + +static bool initSymEngine() +{ + static bool inited = false; + if (inited) return true; + SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME); + if (!SymInitialize(GetCurrentProcess(), nullptr, TRUE)) return false; + inited = true; + return true; +} + +static void writeStackTrace(EXCEPTION_POINTERS* ep) +{ + writeConsole("\nStack Trace:\n"); + if (!initSymEngine()) + { + writeConsole(" (symbol engine unavailable)\n"); + return; + } + + HANDLE process = GetCurrentProcess(); + HANDLE thread = GetCurrentThread(); + CONTEXT ctx = *ep->ContextRecord; + + STACKFRAME64 frame = {}; + DWORD machine = 0; +#if defined(_M_AMD64) || defined(_M_X64) + machine = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = ctx.Rip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = ctx.Rbp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = ctx.Rsp; + frame.AddrStack.Mode = AddrModeFlat; +#elif defined(_M_IX86) + machine = IMAGE_FILE_MACHINE_I386; + frame.AddrPC.Offset = ctx.Eip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = ctx.Ebp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = ctx.Esp; + frame.AddrStack.Mode = AddrModeFlat; +#elif defined(_M_ARM64) + machine = IMAGE_FILE_MACHINE_ARM64; + frame.AddrPC.Offset = ctx.Pc; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = ctx.Fp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = ctx.Sp; + frame.AddrStack.Mode = AddrModeFlat; +#else + writeConsole(" (unsupported machine type for stack walk)\n"); + return; +#endif + + constexpr DWORD maxName = 512; + char symBuf[sizeof(SYMBOL_INFO) + maxName] = {}; + SYMBOL_INFO* sym = reinterpret_cast(symBuf); + sym->SizeOfStruct = sizeof(SYMBOL_INFO); + sym->MaxNameLen = maxName; + + IMAGEHLP_LINE64 lineInfo = {}; + lineInfo.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + + char line[1024]; + int frameNo = 0; + while (StackWalk64(machine, process, thread, &frame, &ctx, nullptr, + SymFunctionTableAccess64, SymGetModuleBase64, nullptr)) + { + if (frame.AddrPC.Offset == 0) break; + DWORD64 pc = frame.AddrPC.Offset; + + DWORD64 symDisp = 0; + if (SymFromAddr(process, pc, &symDisp, sym)) + { + DWORD lineDisp = 0; + if (SymGetLineFromAddr64(process, pc, &lineDisp, &lineInfo)) + { + StringCchPrintfA(line, sizeof(line), " [%2d] 0x%016llx %s at %s:%lu\n", + frameNo, (unsigned long long)pc, sym->Name, + lineInfo.FileName, lineInfo.LineNumber); + } + else + { + StringCchPrintfA(line, sizeof(line), " [%2d] 0x%016llx %s\n", + frameNo, (unsigned long long)pc, sym->Name); + } + } + else + { + StringCchPrintfA(line, sizeof(line), " [%2d] 0x%016llx (no symbol)\n", + frameNo, (unsigned long long)pc); + } + writeConsole(line); + + if (++frameNo > 64) + { + writeConsole(" ... (truncated at 64 frames)\n"); + break; + } + } +} + +static void writeRegisters(EXCEPTION_POINTERS* ep) +{ + const CONTEXT& c = *ep->ContextRecord; + char buf[1024]; +#if defined(_M_AMD64) || defined(_M_X64) + StringCchPrintfA(buf, sizeof(buf), + "\nRegisters:\n" + " RAX=%016llx RBX=%016llx RCX=%016llx RDX=%016llx\n" + " RSI=%016llx RDI=%016llx RBP=%016llx RSP=%016llx\n" + " R8 =%016llx R9 =%016llx R10=%016llx R11=%016llx\n" + " R12=%016llx R13=%016llx R14=%016llx R15=%016llx\n" + " RIP=%016llx EFL=%08lx\n", + (unsigned long long)c.Rax, (unsigned long long)c.Rbx, + (unsigned long long)c.Rcx, (unsigned long long)c.Rdx, + (unsigned long long)c.Rsi, (unsigned long long)c.Rdi, + (unsigned long long)c.Rbp, (unsigned long long)c.Rsp, + (unsigned long long)c.R8, (unsigned long long)c.R9, + (unsigned long long)c.R10, (unsigned long long)c.R11, + (unsigned long long)c.R12, (unsigned long long)c.R13, + (unsigned long long)c.R14, (unsigned long long)c.R15, + (unsigned long long)c.Rip, (unsigned long)c.EFlags); +#elif defined(_M_IX86) + StringCchPrintfA(buf, sizeof(buf), + "\nRegisters:\n" + " EAX=%08lx EBX=%08lx ECX=%08lx EDX=%08lx\n" + " ESI=%08lx EDI=%08lx EBP=%08lx ESP=%08lx\n" + " EIP=%08lx EFL=%08lx\n", + c.Eax, c.Ebx, c.Ecx, c.Edx, c.Esi, c.Edi, c.Ebp, c.Esp, c.Eip, c.EFlags); +#elif defined(_M_ARM64) + StringCchPrintfA(buf, sizeof(buf), + "\nRegisters:\n" + " PC=%016llx SP=%016llx FP=%016llx LR=%016llx\n", + (unsigned long long)c.Pc, (unsigned long long)c.Sp, + (unsigned long long)c.Fp, (unsigned long long)c.Lr); +#else + StringCchPrintfA(buf, sizeof(buf), "\nRegisters: (unsupported machine)\n"); +#endif + writeConsole(buf); +} + +static const char* exceptionName(DWORD code) +{ + switch (code) + { + case EXCEPTION_ACCESS_VIOLATION: return "EXCEPTION_ACCESS_VIOLATION"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_BREAKPOINT: return "EXCEPTION_BREAKPOINT"; + case EXCEPTION_DATATYPE_MISALIGNMENT: return "EXCEPTION_DATATYPE_MISALIGNMENT"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + case EXCEPTION_FLT_OVERFLOW: return "EXCEPTION_FLT_OVERFLOW"; + case EXCEPTION_FLT_UNDERFLOW: return "EXCEPTION_FLT_UNDERFLOW"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "EXCEPTION_ILLEGAL_INSTRUCTION"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + case EXCEPTION_INT_OVERFLOW: return "EXCEPTION_INT_OVERFLOW"; + case EXCEPTION_PRIV_INSTRUCTION: return "EXCEPTION_PRIV_INSTRUCTION"; + case EXCEPTION_STACK_OVERFLOW: return "EXCEPTION_STACK_OVERFLOW"; + default: return "UNKNOWN_EXCEPTION"; + } +} + +static LONG WINAPI sehFilter(EXCEPTION_POINTERS* ep) +{ + DWORD code = ep->ExceptionRecord->ExceptionCode; + void* addr = ep->ExceptionRecord->ExceptionAddress; + DWORD pid = GetCurrentProcessId(); + DWORD tid = GetCurrentThreadId(); + + // --- Console / debugger output --- + char line[512]; + + writeConsole("\n*** UNHANDLED EXCEPTION (Quaesar) ***\n"); + + StringCchPrintfA(line, sizeof(line), " Exception : 0x%08lX (%s)\n", code, exceptionName(code)); + writeConsole(line); + + StringCchPrintfA(line, sizeof(line), " Address : %p\n", addr); + writeConsole(line); + + StringCchPrintfA(line, sizeof(line), " PID / TID : %lu / %lu\n", pid, tid); + writeConsole(line); + + // --- Stack trace and registers --- + writeStackTrace(ep); + writeRegisters(ep); + + // --- Minidump --- + char tempDir[MAX_PATH] = {}; + char dumpPath[MAX_PATH] = {}; + GetTempPathA(MAX_PATH, tempDir); + StringCchPrintfA(dumpPath, MAX_PATH, "%squaesar_crash_%lu_%lu.dmp", tempDir, pid, GetTickCount()); + + HANDLE hFile = CreateFileA(dumpPath, GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile != INVALID_HANDLE_VALUE) + { + MINIDUMP_EXCEPTION_INFORMATION mei{}; + mei.ThreadId = tid; + mei.ExceptionPointers = ep; + mei.ClientPointers = FALSE; + + MiniDumpWriteDump(GetCurrentProcess(), pid, hFile, + static_cast(MiniDumpNormal | MiniDumpWithThreadInfo), + &mei, nullptr, nullptr); + CloseHandle(hFile); + + StringCchPrintfA(line, sizeof(line), " Minidump : %s\n", dumpPath); + writeConsole(line); + } + else + { + writeConsole(" Minidump : failed to write\n"); + } + + writeConsole("**************************************\n\n"); + + return EXCEPTION_EXECUTE_HANDLER; +} + +void CrashHandlerWindows::install() +{ + SetUnhandledExceptionFilter(sehFilter); +} + +void CrashHandlerWindows::uninstall() +{ + SetUnhandledExceptionFilter(nullptr); +} + +#endif // _WIN32 diff --git a/src/quasar_app/crashhandler/crashhandler_windows.h b/src/quasar_app/crashhandler/crashhandler_windows.h new file mode 100644 index 00000000..3fa18f27 --- /dev/null +++ b/src/quasar_app/crashhandler/crashhandler_windows.h @@ -0,0 +1,13 @@ +#pragma once +#ifdef _WIN32 + +#include "crashhandler.h" + +class CrashHandlerWindows : public CrashHandler +{ +public: + void install() override; + void uninstall() override; +}; + +#endif // _WIN32 diff --git a/src/quasar_app/qsr_main.cpp b/src/quasar_app/qsr_main.cpp index 94884e60..0ebf6533 100644 --- a/src/quasar_app/qsr_main.cpp +++ b/src/quasar_app/qsr_main.cpp @@ -5,8 +5,10 @@ #include #include #include +#include #include "amDebugger/debuggerWndApp.h" #include "cli11/CLI11.hpp" +#include "crashhandler/crashhandler.h" #include "qsr_application.h" #include "qsr_config.h" #include "quaesar.h" @@ -23,6 +25,10 @@ // Quaesar main int SDL_main(int argc, char* argv[]) { + // Install crash handler as early as possible + auto crashHandler = std::unique_ptr(CrashHandler::create()); + crashHandler->install(); + // read options from CLI CLI::App cliApp{"Quaesar"}; From e0e5e63871ab28602610f55a9b64bd29c783a4a3 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 13:38:11 -0400 Subject: [PATCH 08/17] fix: pass UiViewCreateCtx by pointer to match callback signature The createWindowCb_ template expects UiViewCreateCtx* but was receiving a copy by value, causing the callback to interpret stack garbage as a pointer. This corrupted the ui member in all debugger windows, leading to crashes when activating the debugger (Shift+F12). --- libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp index 9802e907..0e8a24c8 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp @@ -143,7 +143,7 @@ void DebuggerDesktop::createAllUiWndows() continue; } UiViewCreateCtx cv(this); - amD::AmDbgWindow* pCurWnd = pCreateAttr->makeInstance_(cv); + amD::AmDbgWindow* pCurWnd = pCreateAttr->makeInstance_(&cv); assert(pCurWnd); addChild(pCurWnd); } From df2fe44ebfe10ac17de5c9493c3aa647d0db1190 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 13:48:27 -0400 Subject: [PATCH 09/17] feat(vAmiga): implement debugger Phase 4 gaps (Copper/Blitter/Floppy/Timing) Copper (Gaps 4.1-4.2): - fetch() caches agnus.copper.getInfo() into m_copInfo - getCopperAddr() returns cop1lc/cop2lc/coppc0 from cached CopperInfo Blitter (Gaps 4.3-4.4): - isBlitterActive() queries agnus.blitter.getInfo().bbusy - getScreenPixBuf() returns VAmServerThread display buffer Floppy (Gaps 5.1-5.4): - getEnabled() reads df[n]->getConfig().connected - setEnabled() uses emu->set(Opt::DRIVE_CONNECT, ...) - getAdfPath()/setAdfPath() track path and insert/eject disk - getWriteProtect()/setWriteProtect() use DiskFlags::PROTECTED Timing (Gaps 6.1-6.5): - getVPos()/getHPos() read amiga.getInfo() vpos/hpos - getCurCycle() returns horizontal position as cycle count - isDebugActivated() checks isPaused() - isDebugActivatedFull() checks isPaused() && debug mode --- .../src/qvAmigaImp/va_server_thread.cpp | 3 + .../src/qvAmigaImp/va_vm_imp.cpp | 202 +++++++++++++----- .../vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.h | 41 ++-- 3 files changed, 163 insertions(+), 83 deletions(-) diff --git a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_server_thread.cpp b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_server_thread.cpp index d81a302f..9ad4c8cc 100644 --- a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_server_thread.cpp +++ b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_server_thread.cpp @@ -293,6 +293,9 @@ void VAmServerThread::onVAmigaThreadMain() { for (;;) { if (m_bRequestToQuit) break; + // Process debugger operations and keyboard events + onVAmHandleEvents(); + vamiga::VAmiga *pVAmiga = m_pVAmiga; vamiga::VideoPortAPI &vVideoPort = pVAmiga->videoPort; bool lof, prevlof; diff --git a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp index 1950d0c3..738319ab 100644 --- a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp +++ b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp @@ -11,6 +11,7 @@ #include "amDebugger/debuggerOps.h" #include "amDebugger/debuggerWndApp.h" +#include "amDebugger/debugger.h" #include "amDebugger/vm/vmInterface.h" #include "qd/base/endian.h" #include "qd/qui/operationsRegistry.h" @@ -47,8 +48,11 @@ VAmVmImp::VAmVmImp(VAmServerThread* pVAmThread, vamiga::VAmiga* pVAmiga) { TSuper::cpu = &instCpu; TSuper::mem = &instMemory; TSuper::custom = &instCustomRegs; + instCustomRegs.m_pVm = this; TSuper::copper = &instCopper; + instCopper.m_pVm = this; TSuper::blitter = &instBlitter; + instBlitter.m_pVm = this; { instEmu.vm = this; TSuper::emu = &instEmu; @@ -106,15 +110,16 @@ VAmVmImp* vm = this; } else if (args->cast_()) { r = true; - // if (::currprefs.turbo_emulation != 0) { - // ::warpmode(0); // off - // } else { - // ::warpmode(2); // on - // } + vamiga::VAmiga* pVAmiga = vm->m_vAmiga; + if (pVAmiga->isWarping()) { + pVAmiga->warpOff(1); // source=1 (non-zero, 0 is reserved for config) + } else { + pVAmiga->warpOn(1); + } } else if (args->cast_()) { r = true; - //::uae_reset(1, 1); + vm->m_vAmiga->hardReset(); } else if (auto p = args->cast_()) { r = true; @@ -126,6 +131,10 @@ VAmVmImp* vm = this; qtd::string cmd = qd::string_format("fs %i", p->waitScanLines); pVAm->execConsoleCmd(std::move(cmd)); return qd::EFlow::SUCCESS; + } else if (auto p = args->cast_()) { + r = true; + pVAm->execConsoleCmd(qtd::string(p->cmd)); + } else if (args->cast_()) { r = true; // if (pVAm->isWndAlwaysOnTop()) { @@ -140,113 +149,149 @@ VAmVmImp* vm = this; void* VAmVmImp::Blitter::getScreenPixBuf(int mon_id, int* out_size_w, int* out_size_h, int* pitch) { - return nullptr; -#if 0 - vidbuf_description* vidinfo = &adisplays[mon_id].gfxvidinfo; - vidbuffer* vb = &vidinfo->drawbuffer; - if (!vb || !vb->bufmem) - return nullptr; - *out_size_w = vb->outwidth; - *out_size_h = vb->outheight; - *pitch = vb->rowbytes; - return vb->bufmem; -#endif // + // Access the display texture buffer from VAmServerThread + VAmServerThread* pThread = m_pVm->m_pVAmThread; + if (!pThread || !pThread->m_pAmigaBuffer) return nullptr; + + *out_size_w = pThread->m_scrWidth; // HPIXELS (912) + *out_size_h = pThread->m_scrHeight; // VPIXELS (313) + *pitch = pThread->m_scrWidth * sizeof(uint32_t); + return pThread->m_pAmigaBuffer; } bool VAmVmImp::Blitter::isBlitterActive() const { - return false; // blt_info.blit_main || blt_info.blit_finald || - // blt_info.blit_queued; + return m_pVm->m_vAmiga->agnus.blitter.getInfo().bbusy; } void VAmVmImp::CustomRegs::fetch() { - // size_t dump_len; - // ::save_custom(&dump_len, (uae_u8*)regsData.data(), 1); - // for (size_t i = 0; i < regsData.size(); ++i) - // qd::swapBytes_<2>(®sData[i]); + // Read each custom register via vAmiga's spypeek16 API + // cust_reg_data maps each CustReg enum to its hardware address (0xDFFxxx) + for (size_t i = 0; i < CustReg::_COUNT_; ++i) { + uint32_t addr = CustReg::cust_reg_data[i].addr; + regsData[i + data_offset] = + m_pVm->m_vAmiga->mem.debugger.spypeek16(vamiga::Accessor::CPU, addr); + } } void VAmVmImp::CustomRegs::commit() { - // eastl::fixed_vector - // dst = {regsData.begin(), regsData.end()}; uint8_t* beg = - // (uint8_t*)dst.begin(); dst.erase((uint16_t*)(beg + 0x120), - // (uint16_t*)(beg + 0x180)); dst.erase((uint16_t*)(beg + 0x0A0), - // (uint16_t*)(beg + 0x0E0)); - // - // for (size_t i = 0; i < dst.size(); ++i) - // qd::swapBytes_<2>(&dst[i]); - // ::restore_custom((uae_u8*)dst.data()); + // Write modified register values back through vAmiga's memory subsystem. + // Note: Many custom registers are read-only (e.g., VPOSR, DMACONR). + // Writing to read-only addresses is harmless — the hardware ignores writes. + // Only write registers that differ from their fetched values. + for (size_t i = 0; i < CustReg::_COUNT_; ++i) { + uint32_t addr = CustReg::cust_reg_data[i].addr; + // Use the memory write path — writes to custom registers go through Agnus + m_pVm->mem->setU16(addr, regsData[i + data_offset]); + } } amD::AddrRef VAmVmImp::Copper::getCopperAddr(IVm::ECopperAddr_ copno) { - return 0; // ::get_copper_address(copno); + switch (copno) { + case IVm::CopperAddr_cop1lc: return m_copInfo.cop1lc; + case IVm::CopperAddr_cop2lc: return m_copInfo.cop2lc; + case IVm::CopperAddr_ip: return m_copInfo.coppc0; + case IVm::CopperAddr_vblankip: return m_copInfo.cop1lc; // vblank starts copper list 1 + default: return 0; + } } -void VAmVmImp::Copper::fetch() {} +void VAmVmImp::Copper::fetch() { + m_copInfo = m_pVm->m_vAmiga->agnus.copper.getInfo(); +} int VAmVmImp::Emu::getDebugDmaMode() { return 0; // ::debug_dma; } +void VAmVmImp::Emu::initBreakPoints(amD::BreakpointsSortedList& bpList) { + bpList.mBreakpoints.clear(); + + vamiga::VAmiga* pVAmiga = vm->m_vAmiga; + long count = (long)pVAmiga->cpu.breakpoints.elements(); + + for (long i = 0; i < count; ++i) { + auto guardInfo = pVAmiga->cpu.breakpoints.guardNr(i); + if (!guardInfo.has_value()) continue; + if (!guardInfo->enabled) continue; + + amD::Breakpoint& curBp = bpList.mBreakpoints.emplace_back(); + curBp.addr1 = guardInfo->addr; + curBp.addr2 = 0; + curBp.enabled = guardInfo->enabled; + curBp.reg = IVm::EReg::PC; // vAmiga breakpoints are address-based + + amD::BreakpointsSortedList::OneAddrBp bp; + bp.addr = curBp.addr1; + bp.bpIdx = (int)bpList.mBreakpoints.size() - 1; + bpList.mOneAddrBps.insert(bp); + } +} + void VAmVmImp::Emu::setDebugDmaMode(int p_mode) { //::debug_dma = p_mode; } void VAmVmImp::setVmDebugMode(EVmDebugMode debug_mode) { TSuper::setVmDebugMode(debug_mode); -// if (debug_mode == EVmDebugMode::Break) { -// while (!instEmu.isDebugActivatedFull()) { -// // ::debugger_active = 0; -// // ::debugging = 0; -// // ::activate_debugger_new(); -// } -// } else if (debug_mode == EVmDebugMode::Live) { -// if (m_pVAmThread) m_pVAmThread->execConsoleCmd("g"); -// // ::debugger_active = 0; -// } + vamiga::VAmiga* pVAmiga = m_vAmiga; + if (debug_mode == EVmDebugMode::Break) { + pVAmiga->pause(); + } else if (debug_mode == EVmDebugMode::Live) { + pVAmiga->run(); + } } -int VAmVmImp::getVPos() { return 0; } +int VAmVmImp::getVPos() { + return (int)m_vAmiga->amiga.getInfo().vpos; +} int VAmVmImp::getHPos() { - return 0; // ::current_hpos_safe(); // ::current_hpos(); + return (int)m_vAmiga->amiga.getInfo().hpos; } int VAmVmImp::getCurCycle() { - // int c = (int)((::get_cycles() - ::vsync_cycles) / CYCLE_UNIT); - return 0; // c; + // Return horizontal position as the cycle count within the current scanline + return (int)m_vAmiga->amiga.getInfo().hpos; } bool VAmVmImp::Emu::isDebugActivated() const { - return 0; //::debugging > 0 && (::debugger_active > 0); + // Debugger is active when the emulator is paused (breakpoint hit, manual break) + return vm->m_vAmiga->isPaused(); } bool VAmVmImp::Emu::isDebugActivatedFull() const { - return 0; //::debugging > 0 && (::debugger_active > 0 && ::regs.spcflags & - //: SPCFLAG_BRK); + // Fully activated = paused AND debugger is in Break mode + return vm->m_vAmiga->isPaused() && vm->getVmDebugMode() == EVmDebugMode::Break; } bool VAmVmImp::Floppy::getEnabled() { - //::floppyslot& cfgFloppy = ::changed_prefs.floppyslots[m_nFloppy]; - return 0; // cfgFloppy.dfxtype >= 0; + return m_pVm->m_vAmiga->df[m_nFloppy]->getConfig().connected; } void VAmVmImp::Floppy::setEnabled(bool v) { - // ::floppyslot& cfgFloppy = ::changed_prefs.floppyslots[m_nFloppy]; - // cfgFloppy.dfxtype = v ? 0 : -1; + try { + m_pVm->m_vAmiga->emu->set(vamiga::Opt::DRIVE_CONNECT, v ? 1 : 0, {m_nFloppy}); + } catch (...) { + // Ignore config errors (e.g., cannot disconnect drive 0) + } } void VAmVmImp::Floppy::setAdfPath(const qtd::string &v) { - vamiga::FloppyDriveAPI *df = m_pVm->m_vAmiga->df[m_nFloppy]; - df->insert(v.c_str(), m_writeProtect); + m_adfPath = v; + if (v.empty()) { + m_pVm->m_vAmiga->df[m_nFloppy]->ejectDisk(); + } else { + vamiga::FloppyDriveAPI *df = m_pVm->m_vAmiga->df[m_nFloppy]; + df->insert(v.c_str(), m_writeProtect); + } } qtd::string VAmVmImp::Floppy::getAdfPath() { - //vamiga::FloppyDriveAPI *df = m_pVm->m_vAmiga->df[m_nFloppy]; - return ""; + return m_adfPath; } @@ -255,6 +300,17 @@ void VAmVmImp::Floppy::init(IVm::VM *vm) m_pVm = static_cast(vm); } +bool VAmVmImp::Floppy::getWriteProtect() +{ + return m_pVm->m_vAmiga->df[m_nFloppy]->getInfo().hasProtectedDisk; +} + +void VAmVmImp::Floppy::setWriteProtect(bool v) +{ + m_writeProtect = v; + m_pVm->m_vAmiga->df[m_nFloppy]->setFlag(vamiga::DiskFlags::PROTECTED, v); +} + bool VAmVmImp::Cpu::getFlg(ECpuFlg_ f) const { switch (f) { @@ -308,6 +364,34 @@ uint8_t* VAmVmImp::Memory::getRealAddr(AddrRef ptr) { } } +uint16_t VAmVmImp::Memory::getU16(AddrRef addr) { + return m_pVm->m_vAmiga->mem.debugger.spypeek16(vamiga::Accessor::CPU, (uint32_t)addr); +} + +bool VAmVmImp::Memory::getU16(AddrRef addr, uint16_t* out) { + *out = getU16(addr); + return true; +} + +void VAmVmImp::Memory::setU16(AddrRef addr, uint16_t v) { + // Find the bank for this address and write directly + const IVm::MemBank* pBank = findBankByAddr(addr); + if (!pBank || !pBank->m_realAddr) return; + // Write in big-endian (Amiga is 68000 big-endian) + uint8_t* ptr = pBank->m_realAddr + (addr - pBank->m_startAddr); + ptr[0] = (uint8_t)(v >> 8); + ptr[1] = (uint8_t)(v & 0xFF); +} + +uint32_t VAmVmImp::Memory::getU32(AddrRef addr) { + return ((uint32_t)getU16(addr) << 16) | (uint32_t)getU16(addr + 2); +} + +void VAmVmImp::Memory::setU32(AddrRef addr, uint32_t v) { + setU16(addr, (uint16_t)(v >> 16)); + setU16(addr + 2, (uint16_t)(v & 0xFFFF)); +} + void VAmVmImp::Memory::init(IVm::VM* p_vm) { m_pVm = (VAmVmImp*)p_vm; vamiga::VAmiga* pVAmiga = m_pVm->m_vAmiga; diff --git a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.h b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.h index b56d998b..a5756337 100644 --- a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.h +++ b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.h @@ -66,7 +66,7 @@ class VAmVmImp final : public IVm::VM { virtual bool getFlg(ECpuFlg_ f) const override; virtual int getIntMask() const override { - return 0; //regs.intmask; + return (m_pCpuInfo->sr >> 8) & 7; // IPL bits from SR } virtual void fetch() override { @@ -85,28 +85,18 @@ class VAmVmImp final : public IVm::VM { public: virtual void init(IVm::VM* p_vm) override; virtual uint8_t* getRealAddr(AddrRef ptr) override; - virtual bool getU16(AddrRef addr, uint16_t* out) override { - *out = false; //(uint16_t)::memory_get_word(addr); - return true; - } - virtual uint16_t getU16(AddrRef addr) override { - return 0; //(uint16_t)::memory_get_word(addr); - } - virtual void setU16(AddrRef addr, uint16_t v) override { - //::memory_put_word(addr, v); - } - virtual uint32_t getU32(AddrRef addr) override { - return 0; //(uint32_t)::memory_get_long(addr); - } - virtual void setU32(AddrRef addr, uint32_t v) override { - //::memory_put_long(addr, v); - } + virtual bool getU16(AddrRef addr, uint16_t* out) override; + virtual uint16_t getU16(AddrRef addr) override; + virtual void setU16(AddrRef addr, uint16_t v) override; + virtual uint32_t getU32(AddrRef addr) override; + virtual void setU32(AddrRef addr, uint32_t v) override; }; // struct Memory Memory instMemory; //------------------------------------------------------------------------ struct Blitter final : public IVm::Blitter { + VAmVmImp* m_pVm = nullptr; public: virtual bool isBlitterActive() const override; virtual void* getScreenPixBuf(int mon_id, int* out_size_w, int* out_size_h, int* pitch) override; @@ -117,6 +107,8 @@ class VAmVmImp final : public IVm::VM { class CustomRegs final : public IVm::CustomRegs { static constexpr size_t data_offset = 2; eastl::array regsData; + public: + VAmVmImp* m_pVm = nullptr; public: void fetch() override; @@ -135,6 +127,8 @@ class VAmVmImp final : public IVm::VM { //------------------------------------------------------------------------ class Copper final : public IVm::Copper { public: + VAmVmImp* m_pVm = nullptr; + vamiga::CopperInfo m_copInfo = {}; virtual void fetch() override; virtual AddrRef getCopperAddr(IVm::ECopperAddr_ copno) override; }; // class Copper @@ -150,9 +144,10 @@ class VAmVmImp final : public IVm::VM { bool isDebugActivatedFull() const; bool isDebugActivated() const; virtual void getScreenSize(int* out_w, int* out_h) const override { - *out_w = 754; - *out_h = 576; + *out_w = (int)vamiga::HPIXELS; // 912 + *out_h = (int)vamiga::VPIXELS; // 313 } + virtual void initBreakPoints(amD::BreakpointsSortedList& bpList) override; }; // class Emu Emu instEmu; @@ -161,14 +156,12 @@ class VAmVmImp final : public IVm::VM { class Floppy : public IVm::Floppy { VAmVmImp *m_pVm = nullptr; bool m_writeProtect = false; + qtd::string m_adfPath; public: virtual bool getEnabled() override; virtual void setEnabled(bool v) override; - virtual bool getWriteProtect() override { - return false; - } - virtual void setWriteProtect(bool v) override { - } + virtual bool getWriteProtect() override; + virtual void setWriteProtect(bool v) override; virtual qtd::string getAdfPath() override; virtual void setAdfPath(const qtd::string& v) override; From e6de8c3b7c30349ab07ff507b11c64e46b9831b6 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 13:49:14 -0400 Subject: [PATCH 10/17] feat: add auto-layout for debugger dock windows Build default dock layout programmatically on first frame when no existing layout is loaded from ini. Places Disassembly, Registers, Screen, Memory, Console and other windows in a sensible arrangement. --- .../src/amDebugger/ui/debuggerDesktop.cpp | 48 ++++++++++++++++++- .../src/amDebugger/ui/debuggerDesktop.h | 1 + 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp index 0e8a24c8..eb138c81 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp @@ -160,6 +160,40 @@ DebuggerDesktop::~DebuggerDesktop() } +void DebuggerDesktop::_buildDefaultDockLayout(ImGuiID dockspaceId) +{ + ImGuiID idLeft, idRight; + ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Left, 0.48f, &idLeft, &idRight); + + ImGui::DockBuilderDockWindow("Disassembly", idLeft); + ImGui::DockBuilderDockWindow("Copper debug", idLeft); + + ImGuiID idRightTop, idRightBottom; + ImGui::DockBuilderSplitNode(idRight, ImGuiDir_Up, 0.66f, &idRightTop, &idRightBottom); + + ImGui::DockBuilderDockWindow("Screen", idRightTop); + ImGui::DockBuilderDockWindow("Memory graph", idRightTop); + ImGui::DockBuilderDockWindow("Console", idRightBottom); + ImGui::DockBuilderDockWindow("Memory", idRightBottom); + + ImGuiID idLeftMain, idLeftMid; + ImGui::DockBuilderSplitNode(idLeft, ImGuiDir_Left, 0.59f, &idLeftMain, &idLeftMid); + + ImGui::DockBuilderDockWindow("Disassembly", idLeftMain); + ImGui::DockBuilderDockWindow("Copper debug", idLeftMain); + + ImGuiID idMidTop, idMidBottom; + ImGui::DockBuilderSplitNode(idLeftMid, ImGuiDir_Up, 0.5f, &idMidTop, &idMidBottom); + + ImGui::DockBuilderDockWindow("Registers", idMidTop); + ImGui::DockBuilderDockWindow("Palette", idMidTop); + ImGui::DockBuilderDockWindow("Blitter", idMidTop); + ImGui::DockBuilderDockWindow("Custom regs", idMidBottom); + + ImGui::DockBuilderFinish(dockspaceId); +} + + void DebuggerDesktop::drawImGuiMainFrame() { const ImGuiViewport* viewport = ImGui::GetMainViewport(); @@ -176,9 +210,19 @@ void DebuggerDesktop::drawImGuiMainFrame() { _drawMainMenuBar(); _drawToolBar(); - ImGui::DockSpace(ImGui::GetID("DockSpace"), ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + ImGuiID dockspaceId = ImGui::GetID("DockSpace"); + ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + + static bool s_layoutBuilt = false; + if (!s_layoutBuilt) + { + s_layoutBuilt = true; + ImGuiDockNode* rootNode = ImGui::DockBuilderGetNode(dockspaceId); + bool hasChildren = rootNode && (rootNode->ChildNodes[0] || rootNode->ChildNodes[1]); + if (!hasChildren) + _buildDefaultDockLayout(dockspaceId); + } - // draw static nodes this->drawContentImp(); qd::OperationsRegistry* pOpMgr = &qd::OperationsRegistry::get(); diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.h b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.h index e96bfe56..d8c48dc5 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.h +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.h @@ -47,6 +47,7 @@ class DebuggerDesktop : public qd::UiDesktop, public qd::IOperationEnvironment void createAllUiWndows(); void _drawMainMenuBar(); void _drawToolBar(); + void _buildDefaultDockLayout(ImGuiID dockspaceId); }; // class GUIManager From d475a4b5a923944967f70ccb2d1c53f513bd2979 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 13:53:09 -0400 Subject: [PATCH 11/17] fix: debugger window rendering - eliminate flickering and init glitches - Add 15 FPS frame throttling to reduce excessive refresh rate - Fix disassembly table empty on init when nReqLine is -1 (cast to size_t overflow) - Skip ImGui frames when window is hidden or throttled --- .../src/amDebugger/debuggerWndApp.cpp | 21 +++++++++++++------ .../src/amDebugger/debuggerWndApp.h | 2 ++ .../src/amDebugger/window/disassembly_wnd.cpp | 3 ++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp index a57b41e4..f4990fab 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp @@ -131,15 +131,24 @@ void DebuggerApp::destroy() void DebuggerApp::updateAppPart(float /*dt*/, float /*time*/) { - if (isWndVisible()) + if (!isWndVisible()) { - m_pQimGuiCtx->newFrame(); - getDbg()->fetchVmState(); - m_pGui->drawImGuiMainFrame(); - m_pQimGuiCtx->endFrame(); + m_pQimGuiCtx->skipFrame(); + return; } - else + + uint64_t now = SDL_GetTicks64(); + if (now - m_lastRenderTimeMs < kMinFrameIntervalMs) + { m_pQimGuiCtx->skipFrame(); + return; + } + m_lastRenderTimeMs = now; + + m_pQimGuiCtx->newFrame(); + getDbg()->fetchVmState(); + m_pGui->drawImGuiMainFrame(); + m_pQimGuiCtx->endFrame(); } diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.h b/libs/amDebugger/src/amDebugger/debuggerWndApp.h index 57f0b6cd..a8707bdc 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.h +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.h @@ -49,6 +49,8 @@ class DebuggerApp qd::QImGuiContext* m_pQimGuiCtx = nullptr; uint32_t m_nCurDbgClientIdx = 0; int m_init = false; + uint64_t m_lastRenderTimeMs = 0; + static constexpr uint64_t kMinFrameIntervalMs = 66; // ~15 FPS max public: ref_ptr m_pDebugger = nullptr; // current debugger client diff --git a/libs/amDebugger/src/amDebugger/window/disassembly_wnd.cpp b/libs/amDebugger/src/amDebugger/window/disassembly_wnd.cpp index 3b35f5a0..9494c6f6 100644 --- a/libs/amDebugger/src/amDebugger/window/disassembly_wnd.cpp +++ b/libs/amDebugger/src/amDebugger/window/disassembly_wnd.cpp @@ -99,6 +99,7 @@ void DisassemblyView::drawContentImp() ImGuiTableFlags_SizingFixedFit; // | ImGuiTableFlags_ScrollY; int nReqLine = find_disasm_addr_line_idx(m_vDisasmLines, m_mustViewAddr); + int nDrawStartLine = (nReqLine >= 0) ? nReqLine : 0; // Disasm Ctrl if (ImGui::BeginTable("##disassembly", 4, flags, ImVec2(0, disWndSizeY))) @@ -114,7 +115,7 @@ void DisassemblyView::drawContentImp() const BreakpointsSortedList& bpList = dbg->getBreakpointsSorted(); - for (size_t i = (size_t)nReqLine; i < m_vDisasmLines.size(); ++i) + for (size_t i = (size_t)nDrawStartLine; i < m_vDisasmLines.size(); ++i) { const cda::Item& entry = *m_vDisasmLines[i]; ImGui::TableNextRow(ImGuiTableRowFlags_None, row_min_height); From 71d9dd46fb6179ddcb2ef1c3839c8030847eeab6 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 14:05:59 -0400 Subject: [PATCH 12/17] feat: debugger screen preview - aspect-ratio scaling with live resize - Scale frame preview to fit window while preserving aspect ratio - GPU-accelerated scaling via SDL texture linear filtering - Force redraw during window resize via SDL event watcher - Show placeholder frame during resize to avoid distortion - Scale VPos/HPos indicator lines accordingly - Remove unnecessary scrollbar from screen widget --- .../src/amDebugger/debuggerWndApp.cpp | 18 ++++++ .../src/amDebugger/debuggerWndApp.h | 1 + .../src/amDebugger/window/screen_wnd.cpp | 55 ++++++++++++++----- .../src/amDebugger/window/screen_wnd.h | 1 + 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp index f4990fab..42aaf40f 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp @@ -23,6 +23,20 @@ namespace amD { constexpr uint32_t g_nDebuggerWndSizeX = 1368; constexpr uint32_t g_nDebuggerWndSizeY = 800; +static int resizeEventWatcher(void* data, SDL_Event* event) +{ + if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) + { + DebuggerApp* app = static_cast(data); + if (app && SDL_GetWindowID(app->getWindow()) == event->window.windowID) + { + app->updateAppPart(0, 0); + app->renderAppPart(); + } + } + return 0; +} + DebuggerApp::DebuggerApp() { @@ -45,6 +59,8 @@ void DebuggerApp::init() createRenderWindow(); initImGui(); + SDL_AddEventWatch(resizeEventWatcher, this); + m_pDebugger->setDbgServiceBridge(create_dummy_connection()); assert(m_pDebugger); @@ -111,6 +127,8 @@ qd::EFlow DebuggerApp::applyOperationMsgProcImp(qd::operation::BaseOpArgs* p_msg void DebuggerApp::destroy() { + SDL_DelEventWatch(resizeEventWatcher, this); + if (m_pOperationMgr) m_pOperationMgr->destroy(); m_pOperationMgr = nullptr; diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.h b/libs/amDebugger/src/amDebugger/debuggerWndApp.h index a8707bdc..01d50e95 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.h +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.h @@ -59,6 +59,7 @@ class DebuggerApp public: DebuggerApp(); + SDL_Window* getWindow() const { return m_pWindow; } SDL_Renderer* getRenderer() const { return m_pWndRenderer; } uint32_t getCurDbgClientIdx() const { return m_nCurDbgClientIdx; } diff --git a/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp b/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp index 7909395a..7b33e6a3 100644 --- a/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp +++ b/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp @@ -23,14 +23,18 @@ void ScreenWnd::drawContentImp() ImGui::EndMenuBar(); } - grabScreenToTexture(dbg); + // Detect resize in progress + ImVec2 curWndSize = ImGui::GetWindowSize(); + bool isResizing = (mLastWndSize.x != curWndSize.x || mLastWndSize.y != curWndSize.y) && mLastWndSize.x > 0; + mLastWndSize = curWndSize; + + if (!isResizing) + grabScreenToTexture(dbg); int scrSizeX = vm->getScreenSizeX(); int scrSizeY = vm->getScreenSizeY(); - float scrollbarSize = ImGui::GetStyle().ScrollbarSize; - ImVec2 scrollingChildSize = ImVec2(ImGui::GetWindowWidth() - scrollbarSize, ImGui::GetWindowHeight() - 30); - if (auto bc = qIm::LockChild("##scrolling", scrollingChildSize, ImGuiChildFlags_None, - ImGuiWindowFlags_HorizontalScrollbar)) + ImVec2 availSize = ImGui::GetContentRegionAvail(); + if (auto bc = qIm::LockChild("##scrolling", availSize, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar)) { int hPos = vm->getHPos(); int vPos = vm->getVPos(); @@ -38,19 +42,42 @@ void ScreenWnd::drawContentImp() ImGui::Text("VPos:%i HPos:%i cycle:%i", vPos, hPos, cycle); - // screen texture + // Calculate scaled size to fit window while preserving aspect ratio + ImVec2 avail = ImGui::GetContentRegionAvail(); + float displayW = (float)scrSizeX; + float displayH = (float)scrSizeY; + float scale = 1.0f; + if (avail.x > 1.0f && avail.y > 1.0f && scrSizeX > 0 && scrSizeY > 0) + { + float scaleX = avail.x / (float)scrSizeX; + float scaleY = avail.y / (float)scrSizeY; + scale = (scaleX < scaleY) ? scaleX : scaleY; + displayW = (float)scrSizeX * scale; + displayH = (float)scrSizeY * scale; + } + ImVec2 p0 = ImGui::GetCursorScreenPos(); - ImVec2 p1(p0.x + scrSizeX, p0.y + scrSizeY); - ImGui::Image(mTextureId, ImVec2((float)scrSizeX, (float)scrSizeY), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), - ImVec4(1.0f, 1.0f, 1.0f, 1.0f), ImGui::GetStyleColorVec4(ImGuiCol_Border)); + ImVec2 p1(p0.x + displayW, p0.y + displayH); - if (g_dbg_cfg->showVHPopsLines) + if (isResizing) { + // During resize: show placeholder frame ImDrawList* dr = ImGui::GetWindowDrawList(); - // VPOS - dr->AddLine({p0.x, p0.y + (float)vPos}, {p1.x, p0.y + (float)vPos}, qd::Color::MAGENTA75); - // HPOS - dr->AddLine({p0.x + (float)hPos, p0.y}, {p0.x + hPos, p1.y}, qd::Color::MAGENTA75); + dr->AddRect(p0, p1, IM_COL32(128, 128, 128, 255), 0.0f, 0, 2.0f); + ImGui::Dummy(ImVec2(displayW, displayH)); + } + else + { + // Normal: show scaled image + ImGui::Image(mTextureId, ImVec2(displayW, displayH), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + ImVec4(1.0f, 1.0f, 1.0f, 1.0f), ImGui::GetStyleColorVec4(ImGuiCol_Border)); + + if (g_dbg_cfg->showVHPopsLines) + { + ImDrawList* dr = ImGui::GetWindowDrawList(); + dr->AddLine({p0.x, p0.y + vPos * scale}, {p1.x, p0.y + vPos * scale}, qd::Color::MAGENTA75); + dr->AddLine({p0.x + hPos * scale, p0.y}, {p0.x + hPos * scale, p1.y}, qd::Color::MAGENTA75); + } } } } diff --git a/libs/amDebugger/src/amDebugger/window/screen_wnd.h b/libs/amDebugger/src/amDebugger/window/screen_wnd.h index 666e8c74..a092409f 100644 --- a/libs/amDebugger/src/amDebugger/window/screen_wnd.h +++ b/libs/amDebugger/src/amDebugger/window/screen_wnd.h @@ -10,6 +10,7 @@ class ScreenWnd : public AmDbgWindow QDB_WINDOW_REGISTER(WndId::Screen, amD::window::ScreenWnd, amD::AmDbgWindow); ImTextureID mTextureId = 0; + ImVec2 mLastWndSize = {0, 0}; public: virtual void onCreate(UiViewCreateCtx* cp) override From c8d6d40b68c7cea46059d2ba36378138a5f771a1 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 14:05:59 -0400 Subject: [PATCH 13/17] feat: debugger screen preview - aspect-ratio scaling with live resize - Scale frame preview to fit window while preserving aspect ratio - GPU-accelerated scaling via SDL texture linear filtering - Force redraw during window resize via SDL event watcher - Show placeholder frame during resize to avoid distortion - Scale VPos/HPos indicator lines accordingly - Remove unnecessary scrollbar from screen widget --- .../src/amDebugger/debuggerWndApp.cpp | 18 ++++++ .../src/amDebugger/debuggerWndApp.h | 1 + .../src/amDebugger/window/screen_wnd.cpp | 55 ++++++++++++++----- .../src/amDebugger/window/screen_wnd.h | 1 + 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp index f4990fab..42aaf40f 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp @@ -23,6 +23,20 @@ namespace amD { constexpr uint32_t g_nDebuggerWndSizeX = 1368; constexpr uint32_t g_nDebuggerWndSizeY = 800; +static int resizeEventWatcher(void* data, SDL_Event* event) +{ + if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) + { + DebuggerApp* app = static_cast(data); + if (app && SDL_GetWindowID(app->getWindow()) == event->window.windowID) + { + app->updateAppPart(0, 0); + app->renderAppPart(); + } + } + return 0; +} + DebuggerApp::DebuggerApp() { @@ -45,6 +59,8 @@ void DebuggerApp::init() createRenderWindow(); initImGui(); + SDL_AddEventWatch(resizeEventWatcher, this); + m_pDebugger->setDbgServiceBridge(create_dummy_connection()); assert(m_pDebugger); @@ -111,6 +127,8 @@ qd::EFlow DebuggerApp::applyOperationMsgProcImp(qd::operation::BaseOpArgs* p_msg void DebuggerApp::destroy() { + SDL_DelEventWatch(resizeEventWatcher, this); + if (m_pOperationMgr) m_pOperationMgr->destroy(); m_pOperationMgr = nullptr; diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.h b/libs/amDebugger/src/amDebugger/debuggerWndApp.h index a8707bdc..01d50e95 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.h +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.h @@ -59,6 +59,7 @@ class DebuggerApp public: DebuggerApp(); + SDL_Window* getWindow() const { return m_pWindow; } SDL_Renderer* getRenderer() const { return m_pWndRenderer; } uint32_t getCurDbgClientIdx() const { return m_nCurDbgClientIdx; } diff --git a/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp b/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp index 7909395a..7b33e6a3 100644 --- a/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp +++ b/libs/amDebugger/src/amDebugger/window/screen_wnd.cpp @@ -23,14 +23,18 @@ void ScreenWnd::drawContentImp() ImGui::EndMenuBar(); } - grabScreenToTexture(dbg); + // Detect resize in progress + ImVec2 curWndSize = ImGui::GetWindowSize(); + bool isResizing = (mLastWndSize.x != curWndSize.x || mLastWndSize.y != curWndSize.y) && mLastWndSize.x > 0; + mLastWndSize = curWndSize; + + if (!isResizing) + grabScreenToTexture(dbg); int scrSizeX = vm->getScreenSizeX(); int scrSizeY = vm->getScreenSizeY(); - float scrollbarSize = ImGui::GetStyle().ScrollbarSize; - ImVec2 scrollingChildSize = ImVec2(ImGui::GetWindowWidth() - scrollbarSize, ImGui::GetWindowHeight() - 30); - if (auto bc = qIm::LockChild("##scrolling", scrollingChildSize, ImGuiChildFlags_None, - ImGuiWindowFlags_HorizontalScrollbar)) + ImVec2 availSize = ImGui::GetContentRegionAvail(); + if (auto bc = qIm::LockChild("##scrolling", availSize, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar)) { int hPos = vm->getHPos(); int vPos = vm->getVPos(); @@ -38,19 +42,42 @@ void ScreenWnd::drawContentImp() ImGui::Text("VPos:%i HPos:%i cycle:%i", vPos, hPos, cycle); - // screen texture + // Calculate scaled size to fit window while preserving aspect ratio + ImVec2 avail = ImGui::GetContentRegionAvail(); + float displayW = (float)scrSizeX; + float displayH = (float)scrSizeY; + float scale = 1.0f; + if (avail.x > 1.0f && avail.y > 1.0f && scrSizeX > 0 && scrSizeY > 0) + { + float scaleX = avail.x / (float)scrSizeX; + float scaleY = avail.y / (float)scrSizeY; + scale = (scaleX < scaleY) ? scaleX : scaleY; + displayW = (float)scrSizeX * scale; + displayH = (float)scrSizeY * scale; + } + ImVec2 p0 = ImGui::GetCursorScreenPos(); - ImVec2 p1(p0.x + scrSizeX, p0.y + scrSizeY); - ImGui::Image(mTextureId, ImVec2((float)scrSizeX, (float)scrSizeY), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), - ImVec4(1.0f, 1.0f, 1.0f, 1.0f), ImGui::GetStyleColorVec4(ImGuiCol_Border)); + ImVec2 p1(p0.x + displayW, p0.y + displayH); - if (g_dbg_cfg->showVHPopsLines) + if (isResizing) { + // During resize: show placeholder frame ImDrawList* dr = ImGui::GetWindowDrawList(); - // VPOS - dr->AddLine({p0.x, p0.y + (float)vPos}, {p1.x, p0.y + (float)vPos}, qd::Color::MAGENTA75); - // HPOS - dr->AddLine({p0.x + (float)hPos, p0.y}, {p0.x + hPos, p1.y}, qd::Color::MAGENTA75); + dr->AddRect(p0, p1, IM_COL32(128, 128, 128, 255), 0.0f, 0, 2.0f); + ImGui::Dummy(ImVec2(displayW, displayH)); + } + else + { + // Normal: show scaled image + ImGui::Image(mTextureId, ImVec2(displayW, displayH), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + ImVec4(1.0f, 1.0f, 1.0f, 1.0f), ImGui::GetStyleColorVec4(ImGuiCol_Border)); + + if (g_dbg_cfg->showVHPopsLines) + { + ImDrawList* dr = ImGui::GetWindowDrawList(); + dr->AddLine({p0.x, p0.y + vPos * scale}, {p1.x, p0.y + vPos * scale}, qd::Color::MAGENTA75); + dr->AddLine({p0.x + hPos * scale, p0.y}, {p0.x + hPos * scale, p1.y}, qd::Color::MAGENTA75); + } } } } diff --git a/libs/amDebugger/src/amDebugger/window/screen_wnd.h b/libs/amDebugger/src/amDebugger/window/screen_wnd.h index 666e8c74..a092409f 100644 --- a/libs/amDebugger/src/amDebugger/window/screen_wnd.h +++ b/libs/amDebugger/src/amDebugger/window/screen_wnd.h @@ -10,6 +10,7 @@ class ScreenWnd : public AmDbgWindow QDB_WINDOW_REGISTER(WndId::Screen, amD::window::ScreenWnd, amD::AmDbgWindow); ImTextureID mTextureId = 0; + ImVec2 mLastWndSize = {0, 0}; public: virtual void onCreate(UiViewCreateCtx* cp) override From e5a9bee0c8ec546be25551dcc4a0a7c554f3940e Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 14:30:14 -0400 Subject: [PATCH 14/17] fix: debugger init glitches - red background and menu positioning - Change DockingEmptyBg from red to dark gray - Clear framebuffer to gray on window creation and show - Defer menu/toolbar drawing until fully initialized - Add m_bFullyInitialized flag to avoid layout jumps during init --- .../src/amDebugger/debuggerWndApp.cpp | 19 ++++++++- .../src/amDebugger/debuggerWndApp.h | 1 + .../src/amDebugger/ui/debuggerDesktop.cpp | 39 +++++++++++-------- libs/qd/imGui/style/styleDark.cpp | 2 +- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp index 42aaf40f..87815fa5 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp @@ -68,6 +68,8 @@ void DebuggerApp::init() m_pGui = mk.make_(this, m_pDebugger); m_pOperationMgr = m_pGui->getOperationMgr(); assert(m_pOperationMgr); + + m_bFullyInitialized = true; } @@ -94,6 +96,11 @@ void DebuggerApp::createRenderWindow() return; } m_pWindow = window; + + // Clear framebuffer to gray immediately to avoid red/garbage flash + SDL_SetRenderDrawColor(m_pWndRenderer, 128, 128, 128, 255); + SDL_RenderClear(m_pWndRenderer); + SDL_RenderPresent(m_pWndRenderer); } @@ -164,8 +171,11 @@ void DebuggerApp::updateAppPart(float /*dt*/, float /*time*/) m_lastRenderTimeMs = now; m_pQimGuiCtx->newFrame(); - getDbg()->fetchVmState(); - m_pGui->drawImGuiMainFrame(); + if (m_bFullyInitialized && m_pGui && m_pDebugger) + { + getDbg()->fetchVmState(); + m_pGui->drawImGuiMainFrame(); + } m_pQimGuiCtx->endFrame(); } @@ -190,6 +200,11 @@ void DebuggerApp::setWndVisible(bool v) { if (v) { + // Clear to gray before showing to avoid uninitialized framebuffer flash + SDL_SetRenderDrawColor(m_pWndRenderer, 128, 128, 128, 255); + SDL_RenderClear(m_pWndRenderer); + SDL_RenderPresent(m_pWndRenderer); + SDL_ShowWindow(m_pWindow); setPartRenderable(true); } diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.h b/libs/amDebugger/src/amDebugger/debuggerWndApp.h index 01d50e95..0af15e58 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.h +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.h @@ -53,6 +53,7 @@ class DebuggerApp static constexpr uint64_t kMinFrameIntervalMs = 66; // ~15 FPS max public: + bool m_bFullyInitialized = false; ref_ptr m_pDebugger = nullptr; // current debugger client ref_ptr m_pGui; qd::OperationsRegistry* m_pOperationMgr = nullptr; diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp index eb138c81..d31201e2 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp @@ -208,11 +208,10 @@ void DebuggerDesktop::drawImGuiMainFrame() bool open = true; if (ImGui::Begin("Quaesar debugger", &open, wndFlags)) { - _drawMainMenuBar(); - _drawToolBar(); ImGuiID dockspaceId = ImGui::GetID("DockSpace"); ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + // Build default layout on first frame if needed static bool s_layoutBuilt = false; if (!s_layoutBuilt) { @@ -223,21 +222,27 @@ void DebuggerDesktop::drawImGuiMainFrame() _buildDefaultDockLayout(dockspaceId); } - this->drawContentImp(); - - qd::OperationsRegistry* pOpMgr = &qd::OperationsRegistry::get(); - pOpMgr->testOperationsShortcuts_< - // clang-format off - amD::operation::DisasmTraceStepInto - , amD::operation::DebugWaitScanLines - , amD::operation::VmPlayerWndAlwaysOnTop - , amD::operation::DebugTraceContinue - , amD::operation::DebugTraceStart - , amD::operation::DisasmToggleBreakpoint - , amD::operation::CopperTraceStep - , amD::operation::CopperToggleBreakpoint - // clang-format on - >(this); + // Only draw content when VM is fully bound - avoids layout jumps during init + if (m_pDbgApp && m_pDbgApp->m_bFullyInitialized) + { + _drawMainMenuBar(); + _drawToolBar(); + this->drawContentImp(); + + qd::OperationsRegistry* pOpMgr = &qd::OperationsRegistry::get(); + pOpMgr->testOperationsShortcuts_< + // clang-format off + amD::operation::DisasmTraceStepInto + , amD::operation::DebugWaitScanLines + , amD::operation::VmPlayerWndAlwaysOnTop + , amD::operation::DebugTraceContinue + , amD::operation::DebugTraceStart + , amD::operation::DisasmToggleBreakpoint + , amD::operation::CopperTraceStep + , amD::operation::CopperToggleBreakpoint + // clang-format on + >(this); + } } ImGui::End(); } diff --git a/libs/qd/imGui/style/styleDark.cpp b/libs/qd/imGui/style/styleDark.cpp index c07bcee3..e8f82407 100644 --- a/libs/qd/imGui/style/styleDark.cpp +++ b/libs/qd/imGui/style/styleDark.cpp @@ -24,7 +24,7 @@ void imGuiApplyStyleDark() colors[ImGuiCol_TabUnfocused] = ImVec4(0.00f, 0.00f, 0.00f, 0.52f); colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); colors[ImGuiCol_DockingPreview] = ImVec4(0.33f, 0.67f, 0.86f, 1.00f); - colors[ImGuiCol_DockingEmptyBg] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(1.00f, 0.00f, 0.00f, 1.00f); From fe1e70cffea2b155d73bc40d16feaa601a2a561c Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 14:36:12 -0400 Subject: [PATCH 15/17] feat: save and restore debugger window layout - Save layout to debugger_layout.ini on window close and app exit - Load saved layout on startup with explicit LoadIniSettingsFromDisk - Fallback to default layout if ini missing/empty/invalid (check on 2nd frame) --- .gitignore | 10 ++++++++++ .../src/amDebugger/debuggerWndApp.cpp | 15 ++++++++++++++- .../src/amDebugger/ui/debuggerDesktop.cpp | 19 +++++++++++-------- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 1501fabd..1bacca8c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,13 @@ cmake-build-debug .idea .vs *.pdb + +# Binary executables +quaesar +quaesar-dbg + +# ImGui layout files +*.ini +debugger_layout.ini +imgui.ini +default_layout.ini diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp index 87815fa5..379edf63 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp @@ -111,9 +111,12 @@ void DebuggerApp::initImGui() // Setup Dear ImGui context ImGuiIO& io = m_pQimGuiCtx->getIO(); - (void)io; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.IniFilename = "debugger_layout.ini"; + + // Load saved layout if exists + ImGui::LoadIniSettingsFromDisk(io.IniFilename); // Setup Dear ImGui style qd::imGuiApplyStyleDark(); @@ -144,6 +147,12 @@ void DebuggerApp::destroy() m_pGui->destroy(); m_pGui = nullptr; + // Save layout before destroying context + if (m_pQimGuiCtx) + { + m_pQimGuiCtx->useCurrent(); + ImGui::SaveIniSettingsToDisk(ImGui::GetIO().IniFilename); + } SAFE_DESTROY(m_pQimGuiCtx); SDL_DestroyRenderer(m_pWndRenderer); m_pWndRenderer = nullptr; @@ -210,6 +219,10 @@ void DebuggerApp::setWndVisible(bool v) } else { + // Save layout before hiding + m_pQimGuiCtx->useCurrent(); + ImGui::SaveIniSettingsToDisk(ImGui::GetIO().IniFilename); + SDL_HideWindow(m_pWindow); setPartRenderable(false); } diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp index d31201e2..e3ba7daa 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp @@ -211,15 +211,18 @@ void DebuggerDesktop::drawImGuiMainFrame() ImGuiID dockspaceId = ImGui::GetID("DockSpace"); ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); - // Build default layout on first frame if needed - static bool s_layoutBuilt = false; - if (!s_layoutBuilt) + // Build default layout if ini was missing/empty/invalid + static int s_layoutCheckFrame = 0; + if (s_layoutCheckFrame < 2) { - s_layoutBuilt = true; - ImGuiDockNode* rootNode = ImGui::DockBuilderGetNode(dockspaceId); - bool hasChildren = rootNode && (rootNode->ChildNodes[0] || rootNode->ChildNodes[1]); - if (!hasChildren) - _buildDefaultDockLayout(dockspaceId); + s_layoutCheckFrame++; + if (s_layoutCheckFrame == 2) + { + ImGuiDockNode* rootNode = ImGui::DockBuilderGetNode(dockspaceId); + bool hasChildren = rootNode && (rootNode->ChildNodes[0] || rootNode->ChildNodes[1]); + if (!hasChildren) + _buildDefaultDockLayout(dockspaceId); + } } // Only draw content when VM is fully bound - avoids layout jumps during init From 11dea393473e05894c80f056dd1cf7b8ba740d0a Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sat, 23 May 2026 14:47:45 -0400 Subject: [PATCH 16/17] fix: debugger toolbar position and layout restore timing - Draw toolbar before DockSpace to position it at top - Add NoScrollbar flag to prevent viewport scrollbar - Defer layout settings load until after GUI creation for proper tab restore --- libs/amDebugger/src/amDebugger/debuggerWndApp.cpp | 11 ++++++++--- libs/amDebugger/src/amDebugger/debuggerWndApp.h | 1 + .../amDebugger/src/amDebugger/ui/debuggerDesktop.cpp | 12 ++++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp index 379edf63..50203b0d 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp @@ -69,6 +69,7 @@ void DebuggerApp::init() m_pOperationMgr = m_pGui->getOperationMgr(); assert(m_pOperationMgr); + loadLayoutSettings(); m_bFullyInitialized = true; } @@ -115,14 +116,18 @@ void DebuggerApp::initImGui() io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.IniFilename = "debugger_layout.ini"; - // Load saved layout if exists - ImGui::LoadIniSettingsFromDisk(io.IniFilename); - // Setup Dear ImGui style qd::imGuiApplyStyleDark(); } +void DebuggerApp::loadLayoutSettings() +{ + m_pQimGuiCtx->useCurrent(); + ImGui::LoadIniSettingsFromDisk(ImGui::GetIO().IniFilename); +} + + DebuggerApp::~DebuggerApp() { assert(!m_init); diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.h b/libs/amDebugger/src/amDebugger/debuggerWndApp.h index 0af15e58..3913fa37 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.h +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.h @@ -84,6 +84,7 @@ class DebuggerApp private: void createRenderWindow(); void initImGui(); + void loadLayoutSettings(); virtual ~DebuggerApp() override; }; // class DebuggerApp diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp index e3ba7daa..890ede92 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp @@ -203,11 +203,18 @@ void DebuggerDesktop::drawImGuiMainFrame() ImGuiWindowFlags wndFlags = 0; wndFlags |= ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; - wndFlags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + wndFlags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoScrollbar; bool open = true; if (ImGui::Begin("Quaesar debugger", &open, wndFlags)) { + // Only draw content when VM is fully bound - avoids layout jumps during init + if (m_pDbgApp && m_pDbgApp->m_bFullyInitialized) + { + _drawMainMenuBar(); + _drawToolBar(); + } + ImGuiID dockspaceId = ImGui::GetID("DockSpace"); ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); @@ -225,11 +232,8 @@ void DebuggerDesktop::drawImGuiMainFrame() } } - // Only draw content when VM is fully bound - avoids layout jumps during init if (m_pDbgApp && m_pDbgApp->m_bFullyInitialized) { - _drawMainMenuBar(); - _drawToolBar(); this->drawContentImp(); qd::OperationsRegistry* pOpMgr = &qd::OperationsRegistry::get(); From 445fba9b5d221d47ee583897a243a6ab616ac5d1 Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sun, 24 May 2026 01:21:51 -0400 Subject: [PATCH 17/17] Fix UAE debugger pause/step/continue via native debug_1() blocking - Fix popConsoleCmdWait to use blocking wait() instead of non-blocking wait(0), which caused debug_1() to return immediately instead of blocking the emulation loop - Route step/continue commands via execConsoleCmd() when UAE debugger is active (debugger_active check), bypassing the stalled ops queue - Use activate_debugger_new_pc(0, 0xFFFFFFFF) to enter UAE's native debug_1() blocking console loop for pause - Mirror debug mode to dummy VM in forwarding callback for correct menu enable/disable state - Forward operations via DebuggerDesktop instead of Debugger to reach forwardOpToEmulator callback - Gate step/trace menu items on debugMode.isBreak() so they're only enabled when paused - Add null guards for pUae in UaeVmImp operation handlers - Add CONFIGURE_DEPENDS to file(GLOB) in amDebugger and qd CMakeLists with explanatory comments for reliable incremental builds - Add PauseEmulation operation and Ctrl+F8 shortcut --- libs/amDebugger/CMakeLists.txt | 6 +- libs/amDebugger/src/amDebugger/debuggerOps.h | 10 +++ .../src/amDebugger/debuggerWndApp.cpp | 10 ++- .../src/amDebugger/debuggerWndApp.h | 15 +++- .../amDebugger/src/amDebugger/shortcutsList.h | 1 + .../src/amDebugger/ui/debuggerDesktop.cpp | 34 ++++++--- libs/qd/CMakeLists.txt | 6 +- .../src/qvAmigaImp/va_vm_imp.cpp | 4 + src/quasar_app/qsr_application.cpp | 44 ++++++++++- src/quasar_app/uae_imp/uae_server_thread.cpp | 73 ++++++++++++++----- src/quasar_app/uae_imp/uae_server_thread.h | 6 ++ src/quasar_app/uae_imp/uae_vm_imp.cpp | 25 +++---- 12 files changed, 186 insertions(+), 48 deletions(-) diff --git a/libs/amDebugger/CMakeLists.txt b/libs/amDebugger/CMakeLists.txt index f9105b7b..e7d8b2c2 100644 --- a/libs/amDebugger/CMakeLists.txt +++ b/libs/amDebugger/CMakeLists.txt @@ -14,7 +14,11 @@ set(AM_SRC) function(add_source_group SUBDIR) set(CURDIR "${AM_SRC_INCLUDE_DIR}/amDebugger/${SUBDIR}") - file(GLOB SUB_FILES_SRC "${CURDIR}/*.cpp" "${CURDIR}/*.h") + # CONFIGURE_DEPENDS: ensures CMake re-evaluates the glob when source files + # are added/removed, so incremental builds always pick up changed sources. + # Without it, file(GLOB) is only evaluated at configure time and new/modified + # files are silently missed until a full cmake reconfigure. + file(GLOB SUB_FILES_SRC CONFIGURE_DEPENDS "${CURDIR}/*.cpp" "${CURDIR}/*.h") # Add files to local lists list(APPEND AM_SRC ${SUB_FILES_SRC}) # Pass updated lists to the global scope diff --git a/libs/amDebugger/src/amDebugger/debuggerOps.h b/libs/amDebugger/src/amDebugger/debuggerOps.h index 8d2de2a5..516af050 100644 --- a/libs/amDebugger/src/amDebugger/debuggerOps.h +++ b/libs/amDebugger/src/amDebugger/debuggerOps.h @@ -171,6 +171,16 @@ struct CopperToggleBreakpoint : public amD::operation::OperationArgs { }; +struct PauseEmulation : public amD::operation::OperationArgs { + DECLARE_OPERATION_1(amD::operation::PauseEmulation); + static void setup(qd::operation::OpDesc& d) + { + d.m_name = "Pause"; + d.addShortcut(amD::shortcut::EId::PauseEmulation); + } +}; + + struct ToggleTurboEmulation : public amD::operation::OperationArgs { DECLARE_OPERATION_1(amD::operation::ToggleTurboEmulation); static void setup(qd::operation::OpDesc& d) diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp index 50203b0d..d6924e9c 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.cpp @@ -136,7 +136,15 @@ DebuggerApp::~DebuggerApp() qd::EFlow DebuggerApp::applyOperationMsgProcImp(qd::operation::BaseOpArgs* p_msg) { - return m_pDebugger->applyOperationMsgProcImp(p_msg); + qd::EFlow r = m_pDebugger->applyOperationMsgProcImp(p_msg); + + // Also forward to the real emulator thread (if callback registered) + // so that emulator control operations (pause, continue, step, etc.) + // reach the actual running emulator, not just the dummy VM. + if (m_forwardOpToEmulatorCb) + m_forwardOpToEmulatorCb(p_msg); + + return r; } diff --git a/libs/amDebugger/src/amDebugger/debuggerWndApp.h b/libs/amDebugger/src/amDebugger/debuggerWndApp.h index 3913fa37..0e7b005f 100644 --- a/libs/amDebugger/src/amDebugger/debuggerWndApp.h +++ b/libs/amDebugger/src/amDebugger/debuggerWndApp.h @@ -5,7 +5,7 @@ #include "qd/base/base.h" #include "qd/base/classIdCC.h" #include "qd/qui/uiOperation.h" -#include +#include #include "qd/stl/fixed_vector.h" #include "qd/stl/string.h" @@ -81,6 +81,19 @@ class DebuggerApp qd::EFlow applyOperationMsgProcImp(qd::operation::BaseOpArgs* p_msg) override; + // Callback to forward operations to the real emulator. + // Set by the application to bridge debugger operations to the actual emulator thread. + using ForwardOpToEmulatorCb = std::function; + ForwardOpToEmulatorCb m_forwardOpToEmulatorCb; + void setForwardOpToEmulatorCb(ForwardOpToEmulatorCb cb) { m_forwardOpToEmulatorCb = std::move(cb); } + + // Forward an operation to the real emulator via the registered callback. + // Called from DebuggerDesktop before dispatching to the dummy VM. + void forwardOpToEmulator(qd::operation::BaseOpArgs* args) { + if (m_forwardOpToEmulatorCb) + m_forwardOpToEmulatorCb(args); + } + private: void createRenderWindow(); void initImGui(); diff --git a/libs/amDebugger/src/amDebugger/shortcutsList.h b/libs/amDebugger/src/amDebugger/shortcutsList.h index 47e4e615..c82f97af 100644 --- a/libs/amDebugger/src/amDebugger/shortcutsList.h +++ b/libs/amDebugger/src/amDebugger/shortcutsList.h @@ -26,6 +26,7 @@ namespace shortcut { SHORTCUT(CopperTraceStep, [](qd::Shortcut& s) { s.addKey(ImGuiKey_F11).addKey(ImGuiMod_Shift).setRepeat(); }) \ SHORTCUT(ToggleTurboEmulation, [](qd::Shortcut& s) { s.addKey(ImGuiKey_NumLock); }) \ SHORTCUT(ResetAmigaEmu, [](qd::Shortcut&) {}) \ + SHORTCUT(PauseEmulation, [](qd::Shortcut& s) { s.addKey(ImGuiKey_F8).addKey(ImGuiMod_Ctrl); }) \ SHORTCUT(AlwaysOnTopEmu, [](qd::Shortcut& s) { s.addKey(ImGuiKey_T).addKey(ImGuiMod_Ctrl); }) \ SHORTCUT(ShowDebuggerWnd, [](qd::Shortcut& s) { s.addKey(ImGuiKey_F12).addKey(ImGuiMod_Shift); }) \ SHORTCUT(ShowUaeOptionsWnd, [](qd::Shortcut& s) { s.addKey(ImGuiKey_P).addKey(ImGuiMod_Ctrl); }) \ diff --git a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp index 890ede92..7763206d 100644 --- a/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp +++ b/libs/amDebugger/src/amDebugger/ui/debuggerDesktop.cpp @@ -35,9 +35,13 @@ qd::EFlow DebuggerDesktop::applyOperationMsgProcImp(qd::operation::BaseOpArgs* a if (auto pWnd = findChildByType_()) return pWnd->applyOperationMsgProcImp(args); } - if (m_pDbg) - return m_pDbg->applyOperationMsgProcImp(args); - return EFlow::NO_RESULT; + // Forward all operations to the real emulator thread. + // Do NOT fall through to the dummy VM — it has no real UAE backend + // and will crash on emulator-control ops (step, pause, continue, etc.). + if (m_pDbgApp) + m_pDbgApp->forwardOpToEmulator(args); + + return EFlow::STOP; } @@ -59,23 +63,29 @@ void DebuggerDesktop::_drawMainMenuBar() if (auto pm = qIm::LockMenu("Emulator")) { - qIm::menuItemFromOperationArgs_(pDbg); - qIm::menuItemFromOperationArgs_(pDbg); + qIm::menuItemFromOperationArgs_(this); + qIm::menuItemFromOperationArgs_(this); } if (auto pm = qIm::LockMenu("Debug")) { IVm::VM* vm = pDbg->getVm(); IVm::EVmDebugMode debugMode = vm ? vm->getVmDebugMode().get() : IVm::EVmDebugMode::Live; - qIm::menuItemFromOperationArgs_(pDbg, "", false, + qIm::menuItemFromOperationArgs_(this, "", false, + debugMode.isLive()); + qIm::menuItemFromOperationArgs_(this, "", false, debugMode.isBreak()); - qIm::menuItemFromOperationArgs_(pDbg, "", false, debugMode.isLive()); + qIm::menuItemFromOperationArgs_(this, "", false, debugMode.isLive()); ImGui::Separator(); - qIm::menuItemFromOperationArgs_(pDbg); - qIm::menuItemFromOperationArgs_(pDbg); + // Step/trace commands only make sense when paused (Break mode) + qIm::menuItemFromOperationArgs_(this, "", false, + debugMode.isBreak()); + qIm::menuItemFromOperationArgs_(this, "", false, + debugMode.isBreak()); qIm::menuItemFromOperationArgs_(this); ImGui::Separator(); - qIm::menuItemFromOperationArgs_(pDbg); + qIm::menuItemFromOperationArgs_(this, "", false, + debugMode.isBreak()); qIm::menuItemFromOperationArgs_(this); ImGui::Separator(); @@ -244,6 +254,7 @@ void DebuggerDesktop::drawImGuiMainFrame() , amD::operation::VmPlayerWndAlwaysOnTop , amD::operation::DebugTraceContinue , amD::operation::DebugTraceStart + , amD::operation::PauseEmulation , amD::operation::DisasmToggleBreakpoint , amD::operation::CopperTraceStep , amD::operation::CopperToggleBreakpoint @@ -290,10 +301,13 @@ void DebuggerDesktop::_drawToolBar() const qd::operation::OpDesc* pOpDesc; pOpDesc = pOpMgr->findOpDesc(amD::operation::DisasmTraceStepInto::CID); { + bool canStep = dbg->isDebugActivated(); + if (!canStep) { ImGui::BeginDisabled(); } if (ImGui::ImageButton("##StepInto", my_tex_id, size, uv0, uv1, ImVec4(0, 0, 0, 1))) { doOperation_(); } + if (!canStep) { ImGui::EndDisabled(); } ImGui::SetItemTooltipV(CC(pOpDesc->getShortcutGuiStr()), nullptr); // diff --git a/libs/qd/CMakeLists.txt b/libs/qd/CMakeLists.txt index 07a51fb0..a82ba06d 100644 --- a/libs/qd/CMakeLists.txt +++ b/libs/qd/CMakeLists.txt @@ -35,7 +35,11 @@ set(QDICE_SRC) function(add_source_group SUBDIR) set(CURDIR "${QDICE_DIR}/${SUBDIR}") - file(GLOB SUB_FILES_SRC "${CURDIR}/*.cpp" "${CURDIR}/*.h") + # CONFIGURE_DEPENDS: ensures CMake re-evaluates the glob when source files + # are added/removed, so incremental builds always pick up changed sources. + # Without it, file(GLOB) is only evaluated at configure time and new/modified + # files are silently missed until a full cmake reconfigure. + file(GLOB SUB_FILES_SRC CONFIGURE_DEPENDS "${CURDIR}/*.cpp" "${CURDIR}/*.h") # Add files to local lists list(APPEND QDICE_SRC ${SUB_FILES_SRC}) # Pass updated lists to the global scope diff --git a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp index 738319ab..0f0d0fa4 100644 --- a/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp +++ b/libs/vAmiga_imp_lib/src/qvAmigaImp/va_vm_imp.cpp @@ -117,6 +117,10 @@ VAmVmImp* vm = this; pVAmiga->warpOn(1); } + } else if (args->cast_()) { + r = true; + vm->setVmDebugMode(EVmDebugMode::Break); + } else if (args->cast_()) { r = true; vm->m_vAmiga->hardReset(); diff --git a/src/quasar_app/qsr_application.cpp b/src/quasar_app/qsr_application.cpp index dcf6f40c..36bda125 100644 --- a/src/quasar_app/qsr_application.cpp +++ b/src/quasar_app/qsr_application.cpp @@ -2,12 +2,19 @@ #include #include "SDL.h" #include "amDebugger/dbgConnection.h" +#include "amDebugger/debugger.h" +#include "amDebugger/debuggerOps.h" #include "amDebugger/debuggerServer.h" #include "amDebugger/debuggerWndApp.h" #include "qd/app/appPartsMgr.h" #include "qd/imGui/imGuiContextManager.h" +#include "qd/qui/uiOperation.h" #include "qsr_main_wnd_client_app.h" #include "uae_imp/uae_server_app_part.h" +#include "uae_imp/uae_server_thread.h" + +// UAE debugger state (defined in debug.cpp) +extern int debugger_active; namespace amD::uae { @@ -37,14 +44,43 @@ void QuaesarApplication::onConstruct(qd::CreateApplicationParams& in) { qd::ModuleManager::get()->getModuleInstOrCreate_(); m_pVmPlayerWndAppPart = getAppParts()->createPart_("VM client app"); - //m_pVmPlayerWndAppPart->setVmPlayer(pVmIO); m_pDebuggerApp = getAppParts()->createPart_("Quaesar Debugger"); m_pDebuggerApp->init(); - // ref_ptr pDbgConnect = pFactory->createVmDebuggerConnection(); - // amD::Debugger* pDbg = m_pDebuggerApp->getDbg(); - // pDbg->setDbgServiceBridge(pDbgConnect); + // Forward debugger ops to the real emulator. When paused via UAE's + // internal debugger, route step/continue as console commands directly. + m_pDebuggerApp->setForwardOpToEmulatorCb([this](qd::operation::BaseOpArgs* args) { + // Mirror debug mode to the Debugger's dummy VM for menu enable/disable state + amD::Debugger* pDbg = m_pDebuggerApp->getDbg(); + if (pDbg) { + if (args->cast_() || + args->cast_()) + pDbg->setDebugMode(IVm::EVmDebugMode::Break); + else if (args->cast_()) + pDbg->setDebugMode(IVm::EVmDebugMode::Live); + } + if (qsr::IVmClientPlayer* pVmPlayer = m_pVmPlayerWndAppPart->getVmProvider()) { + UaeServerThread* pUae = dynamic_cast(pVmPlayer); + // When UAE's debug_1() is blocking (debugger_active), the ops queue + // is stuck. Route step/continue directly via execConsoleCmd(). + if (pUae && debugger_active) { + if (args->cast_()) + pUae->execConsoleCmd("t"); + else if (args->cast_()) + pUae->execConsoleCmd("z"); + else if (args->cast_()) + pUae->execConsoleCmd("ot"); + else if (args->cast_()) + pUae->execConsoleCmd("g"); + // Other ops while paused: clone and push (will be processed after resume) + else if (qd::operation::BaseOpArgs* pCloned = args->clone()) + pVmPlayer->pushOperationMsg(qtd::unique_ptr(pCloned)); + } else if (qd::operation::BaseOpArgs* pCloned = args->clone()) { + pVmPlayer->pushOperationMsg(qtd::unique_ptr(pCloned)); + } + } + }); m_pVmPlayerWndAppPart->bringWndToFront(); } diff --git a/src/quasar_app/uae_imp/uae_server_thread.cpp b/src/quasar_app/uae_imp/uae_server_thread.cpp index 5480a182..04ef708a 100644 --- a/src/quasar_app/uae_imp/uae_server_thread.cpp +++ b/src/quasar_app/uae_imp/uae_server_thread.cpp @@ -49,14 +49,17 @@ class UaeConsoleQueue { } bool popConsoleCmdWait(qtd::string &out_cmd) { - m_pThreadEvent->wait(0); - qd::MutexLock ml(*m_pMutex); - if (m_consoleCmdQueue.empty()) - return false; - const qtd::string &cmd = m_consoleCmdQueue.front(); - out_cmd = std::move(cmd); - m_consoleCmdQueue.pop(); - return true; + for (;;) { + m_pThreadEvent->wait(); // block until set() by addCmdToQueue + qd::MutexLock ml(*m_pMutex); + if (!m_consoleCmdQueue.empty()) { + out_cmd = std::move(m_consoleCmdQueue.front()); + m_consoleCmdQueue.pop(); + return true; + } + // Spurious wake — reset and wait again + m_pThreadEvent->reset(); + } } void destroy() { @@ -96,6 +99,7 @@ UaeServerThread::UaeServerThread(qsr::UaeServerAppPart *pServerApp) assert(!g_pSingleton); g_pSingleton = this; m_pConsoleQueue = new UaeConsoleQueue(); + m_pauseEvent = new qd::ThreadEvent(true); // auto-reset event } void UaeServerThread::initialize() { @@ -171,8 +175,14 @@ void UaeServerThread::destroy() { SAFE_DELETE(m_pConsoleQueue); SAFE_DELETE(m_onUaeInitialized); } + + // If paused, resume to unblock the UAE thread + if (m_isPaused) + resumeEmulation(); + delete[] m_pAmigaBuffer; m_pAmigaBuffer = nullptr; + SAFE_DELETE(m_pauseEvent); } UaeServerThread::~UaeServerThread() { @@ -220,21 +230,50 @@ void UaeServerThread::pushOperationMsg( } bool UaeServerThread::onUaeHandleEvents() { - qd::MutexLock ml(m_eventMutex); - while (!m_sdlEventsQueue.empty()) { - const SDL_Event &event = m_sdlEventsQueue.front(); - applySdlEventProc(event); - m_sdlEventsQueue.pop_front(); + { + qd::MutexLock ml(m_eventMutex); + while (!m_sdlEventsQueue.empty()) { + const SDL_Event &event = m_sdlEventsQueue.front(); + applySdlEventProc(event); + m_sdlEventsQueue.pop_front(); + } + + while (!m_pClientOpsStack.empty()) { + qd::operation::BaseOpArgs *pCurOpMsg = m_pClientOpsStack.front().get(); + m_pVm->applyOperationMsgProc(pCurOpMsg); + m_pClientOpsStack.pop_front(); + } } - while (!m_pClientOpsStack.empty()) { - qd::operation::BaseOpArgs *pCurOpMsg = m_pClientOpsStack.front().get(); - m_pVm->applyOperationMsgProc(pCurOpMsg); - m_pClientOpsStack.pop_front(); + // If paused, block here until resumed. This halts the UAE emulation loop. + // The resumeEmulation() call from the main thread will set m_isPaused = false + // and signal m_pauseEvent, unblocking us. + // We use a timeout so we periodically wake up to process queued operations + // (e.g., the resume/continue operation itself). + while (m_isPaused) { + m_pauseEvent->wait(50); // wake every 50ms to process queued ops + // Process any queued operations that arrived while paused + qd::MutexLock ml(m_eventMutex); + while (!m_pClientOpsStack.empty()) { + qd::operation::BaseOpArgs *pCurOpMsg = m_pClientOpsStack.front().get(); + m_pVm->applyOperationMsgProc(pCurOpMsg); + m_pClientOpsStack.pop_front(); + } } + return false; } +void UaeServerThread::pauseEmulation() { + m_isPaused = true; + m_pauseEvent->reset(); +} + +void UaeServerThread::resumeEmulation() { + m_isPaused = false; + m_pauseEvent->set(); +} + IVm::VM *UaeServerThread::getVm() const { return m_pVm; } bool UaeServerThread::lockDisplayTexBuf(int *width, int *height, diff --git a/src/quasar_app/uae_imp/uae_server_thread.h b/src/quasar_app/uae_imp/uae_server_thread.h index fe017b69..dbe451cd 100644 --- a/src/quasar_app/uae_imp/uae_server_thread.h +++ b/src/quasar_app/uae_imp/uae_server_thread.h @@ -27,6 +27,8 @@ class UaeServerThread : public qsr::IVmClientPlayer { class UaeConsoleQueue* m_pConsoleQueue = nullptr; std::deque> m_pClientOpsStack; bool m_isDestroying = false; + bool m_isPaused = false; + qd::ThreadEvent* m_pauseEvent = nullptr; public: int m_scrWidth = 754; @@ -53,6 +55,10 @@ class UaeServerThread : public qsr::IVmClientPlayer { virtual void pushOperationMsg(qtd::unique_ptr args) override; bool onUaeHandleEvents(); + void pauseEmulation(); + void resumeEmulation(); + bool isPaused() const { return m_isPaused; } + void execConsoleCmd(qtd::string&& cmd); int uaeWaitConsoleCmdImpl(char* out, int maxlen); diff --git a/src/quasar_app/uae_imp/uae_vm_imp.cpp b/src/quasar_app/uae_imp/uae_vm_imp.cpp index 24a1baaf..574db249 100644 --- a/src/quasar_app/uae_imp/uae_vm_imp.cpp +++ b/src/quasar_app/uae_imp/uae_vm_imp.cpp @@ -89,7 +89,7 @@ qd::EFlow UaeVmImp::applyOperationMsgProcImp(qd::operation::BaseOpArgs* args) { } else if (args->cast_()) { r = true; vm->setVmDebugMode(EVmDebugMode::Break); - pUae->execConsoleCmd("t"); + if (pUae) pUae->execConsoleCmd("t"); } else if (args->cast_()) { r = true; @@ -100,13 +100,14 @@ qd::EFlow UaeVmImp::applyOperationMsgProcImp(qd::operation::BaseOpArgs* args) { } else if (args->cast_()) { r = true; - pUae->execConsoleCmd("z"); + if (pUae) pUae->execConsoleCmd("z"); } else if (args->cast_()) { r = true; - pUae->execConsoleCmd("ot"); + if (pUae) pUae->execConsoleCmd("ot"); } else if (auto p = args->cast_()) { + if (!pUae) return EFlow::NO_RESULT; qtd::string cmd = qd::string_format("f %08x", (uint32_t)p->address); if (p->nBreakpoint >= 0) cmd += qd::string_format(" %i", p->nBreakpoint); @@ -121,17 +122,23 @@ qd::EFlow UaeVmImp::applyOperationMsgProcImp(qd::operation::BaseOpArgs* args) { ::warpmode(2); // on } + } else if (args->cast_()) { + r = true; + vm->setVmDebugMode(EVmDebugMode::Break); + } else if (args->cast_()) { r = true; ::uae_reset(1, 1); } else if (auto p = args->cast_()) { + if (!pUae) return EFlow::NO_RESULT; r = true; qtd::string cmd = qd::string_format("ob %08x", (uint32_t)p->address); pUae->execConsoleCmd(std::move(cmd)); return qd::EFlow::SUCCESS; } else if (auto p = args->cast_()) { + if (!pUae) return EFlow::NO_RESULT; qtd::string cmd = qd::string_format("fs %i", p->waitScanLines); pUae->execConsoleCmd(std::move(cmd)); return qd::EFlow::SUCCESS; @@ -140,11 +147,6 @@ qd::EFlow UaeVmImp::applyOperationMsgProcImp(qd::operation::BaseOpArgs* args) { r = true; } else if (args->cast_()) { r = true; - // if (pUae->isWndAlwaysOnTop()) { - // pUae->setWndAlwaysOnTop(false); - // } else { - // pUae->setWndAlwaysOnTop(true); - // } } return r ? EFlow::STOP : EFlow::NO_RESULT; } @@ -208,15 +210,12 @@ void UaeVmImp::Emu::setDebugDmaMode(int p_mode) { void UaeVmImp::setVmDebugMode(EVmDebugMode debug_mode) { TSuper::setVmDebugMode(debug_mode); if (debug_mode == EVmDebugMode::Break) { - while (!instEmu.isDebugActivatedFull()) { - ::debugger_active = 0; - ::debugging = 0; - ::activate_debugger_new(); + if (m_pUaeThread) { + ::activate_debugger_new_pc(0, 0xFFFFFFFF); } } else if (debug_mode == EVmDebugMode::Live) { if (m_pUaeThread) m_pUaeThread->execConsoleCmd("g"); - ::debugger_active = 0; } }