fix(render): keep the whole SDL lifecycle on the render thread

The receiver window never appeared on Wayland. Two causes:

- The window was created on the main thread while event pumping and
  presenting ran on the render thread. SDL's Wayland backend requires a
  window's creation, event processing, drawing, and destruction to
  happen on one thread; a cross-thread surface simply never maps, with
  no error reported. The renderer now lives entirely on the render
  thread, with a condition-variable handshake so ReceiverPipeline::
  start() still reports renderer failures and timeouts.
- On Wayland a window is invisible until the first render commit, so
  even a healthy receiver showed nothing while waiting for a stream.
  The renderer presents one blank frame at init: the window is visible
  immediately, black until video arrives.

Also log 'stream started (WxH)' when the first frame decodes and the
first rendering failure, which is what made the remaining debugging
observable.

Validated headlessly end-to-end: a synthetic solid-color RTP feed drove
the real receiver over localhost UDP; the window mapped, reported
'stream started (320x240)', and a grim screenshot of the window region
showed the fed color (V=198, SATAVG=74 during the red feed). Phase 5
marked complete in docs/PHASES.md; current phase is now Phase 6.
This commit is contained in:
2026-09-07 11:17:35 +02:00
parent 3f8e92c6af
commit d2736b8a94
4 changed files with 86 additions and 15 deletions
+55 -10
View File
@@ -3,6 +3,7 @@
#include "screencast/network/h264_packetizer.h"
#include <chrono>
#include <condition_variable>
#include <deque>
#include <iostream>
#include <memory>
@@ -113,14 +114,6 @@ class ReceiverPipeline::Impl {
}
bool start() {
auto renderer_result = RendererFactory::create(config_.renderer);
if (is_renderer_error(renderer_result)) {
std::cerr << std::format("screencast: renderer creation failed: {}\n",
renderer_error(renderer_result).message);
return false;
}
renderer_ = std::move(renderer_value(renderer_result));
auto decoder_result = DecoderFactory::create(config_.decoder);
if (is_codec_error(decoder_result)) {
std::cerr << std::format("screencast: decoder creation failed: {}\n", codec_error(decoder_result).message);
@@ -134,7 +127,50 @@ class ReceiverPipeline::Impl {
return false;
}
render_thread_ = std::jthread([this](std::stop_token stop_token) { render_loop(std::move(stop_token)); });
// Every SDL call — window creation, event pumping, presenting, and
// destruction — happens on the render thread. The Wayland backend
// does not tolerate cross-thread windows: created on another thread,
// the surface never maps and no window appears.
std::mutex init_mutex;
std::condition_variable init_done_cv;
bool init_done = false;
bool init_ok = false;
render_thread_ =
std::jthread([this, &init_mutex, &init_done_cv, &init_done, &init_ok](std::stop_token stop_token) {
auto renderer_result = RendererFactory::create(config_.renderer);
if (is_renderer_error(renderer_result)) {
std::cerr << std::format("screencast: renderer creation failed: {}\n",
renderer_error(renderer_result).message);
} else {
renderer_ = std::move(renderer_value(renderer_result));
}
{
std::lock_guard lock(init_mutex);
init_ok = !is_renderer_error(renderer_result);
init_done = true;
}
init_done_cv.notify_all();
if (!init_ok) {
return;
}
render_loop(std::move(stop_token));
if (renderer_ != nullptr) {
renderer_->shutdown();
renderer_ = nullptr;
}
});
std::unique_lock lock(init_mutex);
if (!init_done_cv.wait_for(lock, std::chrono::seconds(10), [&init_done] { return init_done; })) {
transport_->stop();
return false;
}
if (!init_ok) {
render_thread_ = std::jthread{};
transport_->stop();
return false;
}
return true;
}
@@ -167,6 +203,10 @@ class ReceiverPipeline::Impl {
return;
}
for (DecodedFrame& decoded : codec_value(decoded_result)) {
if (!stream_started_) {
stream_started_ = true;
std::cerr << std::format("screencast: stream started ({}x{})\n", decoded.width, decoded.height);
}
std::lock_guard lock(queue_mutex_);
// Keep latency low: drop the oldest frame when the queue is full.
if (queue_.size() >= kMaxQueuedFrames) {
@@ -190,7 +230,10 @@ class ReceiverPipeline::Impl {
}
}
if (frame.has_value()) {
(void)renderer_->present(*frame);
if (!renderer_->present(*frame) && !present_error_logged_) {
present_error_logged_ = true;
std::cerr << "screencast: rendering a decoded frame failed\n";
}
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
@@ -207,6 +250,8 @@ class ReceiverPipeline::Impl {
std::jthread render_thread_;
std::mutex queue_mutex_;
std::deque<DecodedFrame> queue_;
bool stream_started_ = false;
bool present_error_logged_ = false;
};
ReceiverPipeline::ReceiverPipeline(ReceiverPipelineConfig config) : impl_(std::make_unique<Impl>(std::move(config))) {}