From 4ea96330586e63e477e61fe2eb8b84c3f0933df9 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 9 Aug 2026 00:09:54 +0200 Subject: [PATCH 1/5] display: hide the SDL host pointer when the guest draws its own cursor When the guest OS draws a hardware cursor into the framebuffer, the SDL host pointer was still shown on top, so the user saw two cursors that did not track each other. Hide the host pointer while a guest cursor is drawn and show it again otherwise, so the user can aim the mouse at the window when no guest cursor is present. --- devices/video/display_sdl.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/devices/video/display_sdl.cpp b/devices/video/display_sdl.cpp index 9262cb8ca9..d51bd55096 100644 --- a/devices/video/display_sdl.cpp +++ b/devices/video/display_sdl.cpp @@ -49,6 +49,7 @@ class Display::Impl { SDL_Texture* disp_texture = 0; SDL_Texture* cursor_texture = 0; SDL_Rect cursor_rect; // destination rectangle for cursor drawing + bool show_host_cursor = true; // desired SDL host pointer visibility int display_w; int display_h; double drawable_w; @@ -466,6 +467,16 @@ void Display::update(std::function conver SDL_RenderClear(impl->renderer); SDL_RenderCopy(impl->renderer, impl->disp_texture, NULL, &impl->dest_rect); + // The guest might draw its own cursor into the framebuffer. In that case + // hide the SDL host pointer so the user doesn't see two cursors that don't + // track each other. Otherwise show the host pointer so the user can still + // aim the mouse at the window before grabbing it. + bool want_host_cursor = !draw_hw_cursor; + if (impl->show_host_cursor != want_host_cursor) { + impl->show_host_cursor = want_host_cursor; + SDL_ShowCursor(want_host_cursor ? SDL_ENABLE : SDL_DISABLE); + } + // draw HW cursor if enabled if (draw_hw_cursor) { impl->cursor_rect.x = cursor_x * impl->renderer_scale_x + impl->dest_rect.x; From 878ea0ce7796536092ebfcbc4c17c252b8364330 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 9 Aug 2026 00:10:24 +0200 Subject: [PATCH 2/5] display: auto-grab the mouse when the guest draws its own cursor While a guest cursor is drawn, the host mouse is now grabbed automatically so the guest cursor can reach every screen area and the host pointer cannot wander off the window. The grab is released again when the guest cursor disappears, and re-established when the window regains focus, because SDL may drop the grab on focus loss. A manual grab with Ctrl+G is sticky and overrides the automatic behavior. --- devices/video/display_sdl.cpp | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/devices/video/display_sdl.cpp b/devices/video/display_sdl.cpp index d51bd55096..cfcd874778 100644 --- a/devices/video/display_sdl.cpp +++ b/devices/video/display_sdl.cpp @@ -50,6 +50,8 @@ class Display::Impl { SDL_Texture* cursor_texture = 0; SDL_Rect cursor_rect; // destination rectangle for cursor drawing bool show_host_cursor = true; // desired SDL host pointer visibility + bool guest_cursor_drawn = false; // guest is drawing its own cursor + bool manual_grab = false; // grab toggled on with Ctrl+G int display_w; int display_h; double drawable_w; @@ -313,6 +315,13 @@ void Display::handle_events(const WindowEvent& wnd_event) { if (wnd_event.window_id == impl->disp_wnd_id) { SDL_SetHint(SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED, "0"); SDL_SetWindowKeyboardGrab(impl->display_wnd, SDL_TRUE); + // When the window (re)gains focus (e.g. the user clicks it), SDL + // may have dropped the relative mouse grab on focus loss. Re-grab + // it so the guest cursor keeps working; this is the reliable + // re-grab trigger instead of polling every frame. + if (impl->guest_cursor_drawn && !impl->manual_grab && !SDL_GetRelativeMouseMode()) { + SDL_SetRelativeMouseMode(SDL_TRUE); + } } break; @@ -395,9 +404,11 @@ void Display::toggle_mouse_grab() { if (SDL_GetRelativeMouseMode()) { SDL_SetRelativeMouseMode(SDL_FALSE); + impl->manual_grab = false; } else { this->update_mouse_grab(true); SDL_SetRelativeMouseMode(SDL_TRUE); + impl->manual_grab = true; } } @@ -467,16 +478,40 @@ void Display::update(std::function conver SDL_RenderClear(impl->renderer); SDL_RenderCopy(impl->renderer, impl->disp_texture, NULL, &impl->dest_rect); - // The guest might draw its own cursor into the framebuffer. In that case - // hide the SDL host pointer so the user doesn't see two cursors that don't - // track each other. Otherwise show the host pointer so the user can still - // aim the mouse at the window before grabbing it. + bool is_grabbed = SDL_GetRelativeMouseMode(); + + // The guest draws its own cursor into the framebuffer (hardware cursor). + // When such a cursor session starts, auto-grab the host mouse so the + // pointer cannot leave the window and the guest cursor can reach every + // screen area. When the session ends, release an automatic grab again so + // the user can move the pointer to other windows. A manual grab (Ctrl+G) + // is sticky and is neither grabbed nor released automatically. + if (draw_hw_cursor) { + if (!impl->guest_cursor_drawn && !impl->manual_grab && !is_grabbed) { + this->update_mouse_grab(true); + SDL_SetRelativeMouseMode(SDL_TRUE); + is_grabbed = true; + } + } else { + if (!impl->manual_grab && is_grabbed) { + SDL_SetRelativeMouseMode(SDL_FALSE); + is_grabbed = false; + } + } + + // The guest draws its own cursor into the framebuffer. Even without a + // grab, keep the SDL host pointer hidden so the user never sees a second + // cursor that does not track the guest one. When the guest cursor is not + // drawn (e.g. firmware), show the host pointer so the user can aim and + // click. bool want_host_cursor = !draw_hw_cursor; if (impl->show_host_cursor != want_host_cursor) { impl->show_host_cursor = want_host_cursor; SDL_ShowCursor(want_host_cursor ? SDL_ENABLE : SDL_DISABLE); } + impl->guest_cursor_drawn = draw_hw_cursor; + // draw HW cursor if enabled if (draw_hw_cursor) { impl->cursor_rect.x = cursor_x * impl->renderer_scale_x + impl->dest_rect.x; From 49b37019c96d9237b2f0c7387ba2d022b469c4ad Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 9 Aug 2026 00:10:32 +0200 Subject: [PATCH 3/5] display: show mouse grab state and shortcut in the window title The window title now reflects whether the mouse is grabbed and how to toggle it, so the user knows how to release the automatic grab and move the pointer to other windows, or how to re-grab it. --- devices/video/display_sdl.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/devices/video/display_sdl.cpp b/devices/video/display_sdl.cpp index cfcd874778..486ca4ef1f 100644 --- a/devices/video/display_sdl.cpp +++ b/devices/video/display_sdl.cpp @@ -51,6 +51,7 @@ class Display::Impl { SDL_Rect cursor_rect; // destination rectangle for cursor drawing bool show_host_cursor = true; // desired SDL host pointer visibility bool guest_cursor_drawn = false; // guest is drawing its own cursor + bool was_grabbed = false; // last reported grab state bool manual_grab = false; // grab toggled on with Ctrl+G int display_w; int display_h; @@ -321,6 +322,7 @@ void Display::handle_events(const WindowEvent& wnd_event) { // re-grab trigger instead of polling every frame. if (impl->guest_cursor_drawn && !impl->manual_grab && !SDL_GetRelativeMouseMode()) { SDL_SetRelativeMouseMode(SDL_TRUE); + this->update_window_title(); } } break; @@ -446,7 +448,9 @@ void Display::update_window_title() std::to_string(impl->display_w) + "x" + std::to_string(impl->display_h) + " " + std::to_string(int(std::round(impl->renderer_scale_x * 100))) + "%"; if (is_grabbed) - new_window_title += " (🖱 Grabbed)"; + new_window_title += " (Grabbed, Ctrl+G to release)"; + else if (impl->guest_cursor_drawn) + new_window_title += " (Press Ctrl+G to grab)"; if (new_window_title != old_window_title) SDL_SetWindowTitle(impl->display_wnd, new_window_title.c_str()); @@ -510,7 +514,12 @@ void Display::update(std::function conver SDL_ShowCursor(want_host_cursor ? SDL_ENABLE : SDL_DISABLE); } - impl->guest_cursor_drawn = draw_hw_cursor; + // Let the window title reflect the current grab/cursor state. + if (is_grabbed != impl->was_grabbed || draw_hw_cursor != impl->guest_cursor_drawn) { + impl->was_grabbed = is_grabbed; + impl->guest_cursor_drawn = draw_hw_cursor; + this->update_window_title(); + } // draw HW cursor if enabled if (draw_hw_cursor) { From 0ce1466e6db9de2471b50c87a01e1dfc52312275 Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 17 Aug 2026 03:14:35 +0200 Subject: [PATCH 4/5] viacuda: accept TIMER_TICKLE and SET_POWER_MESSAGES pseudo commands Log both commands at INFO instead of WARNING. Track the shutdown watchdog count from TIMER_TICKLE and never act on expiry so the guest cannot be powered off spuriously when it stops tickling. --- devices/common/viacuda.cpp | 16 ++++++++++++++-- devices/common/viacuda.h | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/devices/common/viacuda.cpp b/devices/common/viacuda.cpp index a224dc2e94..f56de11b1f 100644 --- a/devices/common/viacuda.cpp +++ b/devices/common/viacuda.cpp @@ -790,7 +790,8 @@ void ViaCuda::pseudo_command() { response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_SET_POWER_MESSAGES: - LOG_F(WARNING, "Cuda: unsupported pseudo command 0x%X SET_POWER_MESSAGES", cmd); + this->power_messages_enabled = !!this->in_buf[2]; + LOG_F(INFO, "Cuda: power messages %s", this->power_messages_enabled ? "enabled" : "disabled"); response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_READ_WRITE_I2C: @@ -798,7 +799,18 @@ void ViaCuda::pseudo_command() { i2c_simple_transaction(this->in_buf[2], &this->in_buf[3], this->in_count - 3); break; case CUDA_TIMER_TICKLE: - LOG_F(WARNING, "Cuda: unsupported pseudo command 0x%X TIMER_TICKLE - Byte Sent: 0x%02x", cmd, this->in_buf[2]); + // The guest re-arms the Cuda shutdown watchdog on every tickle; the + // firmware counts it down at 16 counts/sec and powers the machine off + // when it reaches zero. We track the count so an arm value change is + // visible in the log, but never act on expiry to avoid spurious + // power-offs if the guest stops tickling. + if (this->tickle_value != this->in_buf[2]) { + this->tickle_value = this->in_buf[2]; + LOG_F(INFO, "Cuda: timer tickle set to %d counts (%.1f s)", this->tickle_value, + (double)this->tickle_value / 16.0); + } else { + LOG_F(9, "Cuda: timer tickle"); + } response_header(CUDA_PKT_PSEUDO, 0); break; case CUDA_COMB_FMT_I2C: diff --git a/devices/common/viacuda.h b/devices/common/viacuda.h index 3054dcc790..e37b0dee4b 100644 --- a/devices/common/viacuda.h +++ b/devices/common/viacuda.h @@ -241,6 +241,8 @@ class ViaCuda : public I2CBus { bool one_sec_missed = false; // ERS: missed pkt -> fallback to mode $01 bool file_server = false; bool mono_stable = false; + bool power_messages_enabled = false; + uint8_t tickle_value = 0; // last shutdown watchdog count from the guest uint8_t ipl_level = 0; uint16_t device_mask = 0; From aae7f744145f5a127153292614ca5b171fb4b74b Mon Sep 17 00:00:00 2001 From: probonopd Date: Mon, 17 Aug 2026 03:14:38 +0200 Subject: [PATCH 5/5] soundserver: drain guest sound DMA via cyclic timer when host stream fails to start If the host audio stream cannot be started (e.g. no working output device), the guest sound DMA never advances because the cubeb callback that pulls data is never invoked. The guest sound driver then stalls forever waiting for DMA interrupts and the system hangs during boot. Fall back to draining the DMA via a cyclic timer, as is already done in deterministic mode, so the guest sees DMA progress even though there is no audible output. --- devices/sound/soundserver_cubeb.cpp | 55 ++++++++++++++++++----------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/devices/sound/soundserver_cubeb.cpp b/devices/sound/soundserver_cubeb.cpp index 320bf8fda8..f31cb3019b 100644 --- a/devices/sound/soundserver_cubeb.cpp +++ b/devices/sound/soundserver_cubeb.cpp @@ -167,22 +167,26 @@ static void status_callback(cubeb_stream *stream, void *user_data, cubeb_state s int SoundServer::open_out_stream(uint32_t sample_rate, DmaOutChannel *dma_ch) { - if (is_deterministic) { - impl->deterministic_poll_cb = [dma_ch] { - if (!dma_ch->is_out_active()) { - return; - } - // Drain the DMA buffer, but don't do anything else. - while(1) { - uint8_t *chunk; - uint32_t chunk_size; - if (DmaPullResult::MoreData == dma_ch->pull_data(1024, &chunk_size, &chunk)) { - ; - } else { - break; - } + // Set up a cyclic-timer DMA drain callback. It keeps the guest sound DMA + // advancing even if the host audio stream cannot be started, otherwise + // the guest sound driver would stall forever waiting for DMA interrupts. + impl->deterministic_poll_cb = [dma_ch] { + if (!dma_ch->is_out_active()) { + return; + } + // Drain the DMA buffer, but don't do anything else. + while(1) { + uint8_t *chunk; + uint32_t chunk_size; + if (DmaPullResult::MoreData == dma_ch->pull_data(1024, &chunk_size, &chunk)) { + ; + } else { + break; } - }; + } + }; + + if (is_deterministic) { impl->status = SND_STREAM_OPENED; LOG_F(9, "Deterministic sound output callback set up."); return 0; @@ -228,16 +232,27 @@ int SoundServer::start_out_stream() TimerManager::get_instance()->add_cyclic_timer(MSECS_TO_NSECS(10), impl->deterministic_poll_cb); return 0; } - return cubeb_stream_start(impl->out_stream); + int res = cubeb_stream_start(impl->out_stream); + if (res != CUBEB_OK) { + // Fall back to draining the guest sound DMA via a cyclic timer so + // that the guest sound driver does not stall waiting for DMA progress + // that the failed host stream will never provide. + if (!impl->deterministic_poll_timer) { + impl->deterministic_poll_timer = + TimerManager::get_instance()->add_cyclic_timer(MSECS_TO_NSECS(10), impl->deterministic_poll_cb); + LOG_F(9, "Host sound output stream start failed; falling back to cyclic DMA drain."); + } + } + return res; } void SoundServer::close_out_stream() { - if (!impl->out_stream) - return; - if (is_deterministic) { - LOG_F(9, "Stopping sound output deterministic polling."); + if (impl->deterministic_poll_timer) { TimerManager::get_instance()->cancel_timer(impl->deterministic_poll_timer); + impl->deterministic_poll_timer = 0; + } + if (!impl->out_stream) { impl->status = SND_STREAM_CLOSED; return; }