diff --git a/src/network/host_discovery.cpp b/src/network/host_discovery.cpp index b75c263..ca45d14 100644 --- a/src/network/host_discovery.cpp +++ b/src/network/host_discovery.cpp @@ -182,9 +182,20 @@ namespace { * @brief Thread-safe collector updated by the lwIP mDNS search callback. */ struct DiscoveryCollector { - mutable std::mutex mutex; ///< Guards the partial discovery state shared with the callback. - std::unordered_map servicesByInstance; ///< Partial records keyed by instance domain. - std::unordered_map ipv4ByDomain; ///< Resolved IPv4 addresses keyed by target host domain. + /** + * @brief Construct an empty discovery collector. + */ + DiscoveryCollector() = default; + + private: + friend void remember_ptr_service_instance(DiscoveryCollector *collector, std::string instanceDomain); + friend void remember_srv_service_target(DiscoveryCollector *collector, std::string instanceDomain, std::string targetDomain, uint16_t port); + friend void remember_a_record(DiscoveryCollector *collector, std::string domain, std::string ipv4Address); + friend network::DiscoverHostsResult build_discover_hosts_result(const DiscoveryCollector &collector); + + mutable std::mutex mutex_; ///< Guards the partial discovery state shared with the callback. + std::unordered_map servicesByInstance_; ///< Partial records keyed by instance domain. + std::unordered_map ipv4ByDomain_; ///< Resolved IPv4 addresses keyed by target host domain. }; std::string encoded_domain_to_string(const char *encodedDomain, std::size_t encodedLength) { @@ -235,8 +246,8 @@ namespace { return; } - const std::scoped_lock lock(collector->mutex); - PendingDiscoveredService &service = collector->servicesByInstance[instanceDomain]; + const std::scoped_lock lock(collector->mutex_); + PendingDiscoveredService &service = collector->servicesByInstance_[instanceDomain]; service.instanceDomain = std::move(instanceDomain); if (service.displayName.empty()) { service.displayName = first_dns_label(service.instanceDomain); @@ -248,8 +259,8 @@ namespace { return; } - const std::scoped_lock lock(collector->mutex); - PendingDiscoveredService &service = collector->servicesByInstance[instanceDomain]; + const std::scoped_lock lock(collector->mutex_); + PendingDiscoveredService &service = collector->servicesByInstance_[instanceDomain]; service.instanceDomain = std::move(instanceDomain); if (service.displayName.empty()) { service.displayName = first_dns_label(service.instanceDomain); @@ -263,8 +274,8 @@ namespace { return; } - const std::scoped_lock lock(collector->mutex); - collector->ipv4ByDomain[std::move(domain)] = std::move(ipv4Address); + const std::scoped_lock lock(collector->mutex_); + collector->ipv4ByDomain_[std::move(domain)] = std::move(ipv4Address); } /** @@ -315,14 +326,14 @@ namespace { network::DiscoverHostsResult build_discover_hosts_result(const DiscoveryCollector &collector) { network::DiscoverHostsResult result {}; - const std::scoped_lock lock(collector.mutex); - for (const auto &[instanceDomain, service] : collector.servicesByInstance) { + const std::scoped_lock lock(collector.mutex_); + for (const auto &[instanceDomain, service] : collector.servicesByInstance_) { (void) instanceDomain; if (service.targetDomain.empty() || service.port == 0) { continue; } - const auto addressIterator = collector.ipv4ByDomain.find(service.targetDomain); - if (addressIterator == collector.ipv4ByDomain.end()) { + const auto addressIterator = collector.ipv4ByDomain_.find(service.targetDomain); + if (addressIterator == collector.ipv4ByDomain_.end()) { continue; } diff --git a/src/streaming/ffmpeg_stream_backend.cpp b/src/streaming/ffmpeg_stream_backend.cpp index e6087e7..7ebf4cc 100644 --- a/src/streaming/ffmpeg_stream_backend.cpp +++ b/src/streaming/ffmpeg_stream_backend.cpp @@ -457,7 +457,7 @@ namespace streaming { bool textureNeedsUpload = false; if (const std::uint64_t publishedFrameVersion = video_.publishedFrameVersion.load(); publishedFrameVersion != video_.renderedFrameVersion) { - std::scoped_lock lock(video_.frameMutex); + std::scoped_lock lock(video_.frame_mutex()); if (video_.latestFrameVersion != video_.renderedFrameVersion && video_.latestFrame.width > 0 && video_.latestFrame.height > 0) { std::swap(video_.renderFrame, video_.latestFrame); video_.renderedFrameVersion = video_.latestFrameVersion; @@ -611,7 +611,7 @@ namespace streaming { video_.renderFrame = LatestVideoFrame {}; video_.decodeFrame = LatestVideoFrame {}; { - std::scoped_lock lock(video_.frameMutex); + std::scoped_lock lock(video_.frame_mutex()); video_.latestFrame = LatestVideoFrame {}; video_.latestFrameVersion = 0; } @@ -742,7 +742,7 @@ namespace streaming { std::memcpy(nextFrame.vPlane.data(), frameToPresent->data[2], nextFrame.vPlane.size()); { - std::scoped_lock lock(video_.frameMutex); + std::scoped_lock lock(video_.frame_mutex()); std::swap(video_.latestFrame, video_.decodeFrame); ++video_.latestFrameVersion; video_.publishedFrameVersion.store(video_.latestFrameVersion); diff --git a/src/streaming/ffmpeg_stream_backend.h b/src/streaming/ffmpeg_stream_backend.h index 07a8fe9..554f3c2 100644 --- a/src/streaming/ffmpeg_stream_backend.h +++ b/src/streaming/ffmpeg_stream_backend.h @@ -277,7 +277,20 @@ namespace streaming { std::uint64_t renderedFrameVersion = 0; std::vector convertedBuffer; std::vector packetBuffer; - mutable std::mutex frameMutex; + + /** + * @brief Return the mutex guarding decoded frame publication. + * + * @return Mutex used to exchange frames between decode and render threads. + */ + [[nodiscard]] std::mutex &frame_mutex() const { + return frameMutex_; + } + + private: + mutable std::mutex frameMutex_; ///< Guards latest decoded frame publication between worker and render threads. + + public: SDL_Rect directFramebufferDestination {0, 0, 0, 0}; LatestVideoFrame latestFrame; LatestVideoFrame decodeFrame; diff --git a/src/streaming/session.cpp b/src/streaming/session.cpp index 11fce20..b03ce03 100644 --- a/src/streaming/session.cpp +++ b/src/streaming/session.cpp @@ -75,16 +75,6 @@ namespace { static_cast(A_FLAG | B_FLAG | X_FLAG | Y_FLAG | UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG | LB_FLAG | RB_FLAG | PLAY_FLAG | BACK_FLAG | LS_CLK_FLAG | RS_CLK_FLAG | SPECIAL_FLAG); constexpr uint16_t CONTROLLER_CAPABILITIES = LI_CCAP_ANALOG_TRIGGERS | LI_CCAP_RUMBLE; - struct StreamUiResources { - SDL_Renderer *renderer = nullptr; - TTF_Font *titleFont = nullptr; - TTF_Font *bodyFont = nullptr; - SDL_GameController *controller = nullptr; - streaming::FfmpegStreamBackend mediaBackend {}; - mutable std::mutex controllerMutex; - bool ttfInitialized = false; - }; - struct ControllerSnapshot { int buttonFlags = 0; unsigned char leftTrigger = 0; @@ -95,6 +85,107 @@ namespace { short rightStickY = 0; }; + /** + * @brief Thread-safe owner for the active stream controller handle. + */ + class StreamControllerHandle { + public: + /** + * @brief Replace the current controller handle without closing the old one. + * + * @param controller New controller handle to store. + */ + void reset(SDL_GameController *controller); + + /** + * @brief Close and clear the current controller handle. + */ + void close(); + + /** + * @brief Open a controller when no controller is currently active. + * + * @param deviceIndex SDL joystick device index reported by the add event. + */ + void open_if_needed(int deviceIndex); + + /** + * @brief Close the controller when SDL reports that its joystick was removed. + * + * @param joystickInstanceId SDL joystick instance identifier from the remove event. + */ + void close_if_removed(int joystickInstanceId); + + /** + * @brief Return the raw handle for same-thread startup polling. + * + * @return Current controller handle, or nullptr when no controller is open. + */ + [[nodiscard]] SDL_GameController *startup_poll_handle() const; + + /** + * @brief Read a synchronized controller input snapshot. + * + * @param controllerPresent Optional output set to true when a controller was read. + * @return Current controller input state, or a zeroed snapshot when unavailable. + */ + [[nodiscard]] ControllerSnapshot read_snapshot(bool *controllerPresent) const; + + private: + mutable std::mutex mutex_; ///< Guards controller lifetime and input reads across the stream loop. + SDL_GameController *controller_ = nullptr; ///< Currently opened SDL controller handle. + }; + + struct StreamUiResources { + SDL_Renderer *renderer = nullptr; + TTF_Font *titleFont = nullptr; + TTF_Font *bodyFont = nullptr; + StreamControllerHandle controller; + streaming::FfmpegStreamBackend mediaBackend {}; + bool ttfInitialized = false; + }; + + /** + * @brief Retained connection-protocol messages protected by their own lock. + */ + class StreamProtocolLogBuffer { + public: + /** + * @brief Append one trimmed protocol message to the rolling buffer. + * + * @param message Protocol log message to retain. + */ + void append(std::string message) { + std::scoped_lock lock(mutex_); + if (messages_.size() >= MAX_CONNECTION_PROTOCOL_MESSAGES) { + messages_.pop_front(); + } + messages_.push_back(std::move(message)); + } + + /** + * @brief Return the most recent retained protocol message. + * + * @return Latest message, or an empty string when the buffer is empty. + */ + [[nodiscard]] std::string latest() const { + std::scoped_lock lock(mutex_); + return messages_.empty() ? std::string {} : messages_.back(); + } + + /** + * @brief Remove every retained protocol message. + */ + void clear() { + std::scoped_lock lock(mutex_); + messages_.clear(); + } + + private: + mutable std::mutex mutex_; ///< Guards retained protocol messages. + std::deque messages_; ///< Rolling protocol message buffer. + }; + struct StreamConnectionState { std::atomic currentStage = STAGE_NONE; std::atomic failedStage = STAGE_NONE; @@ -106,8 +197,7 @@ namespace { std::atomic connectionTerminated = false; std::atomic poorConnection = false; std::atomic stopRequested = false; - mutable std::mutex protocolLogMutex; - std::deque recentProtocolMessages; + StreamProtocolLogBuffer protocolLog; ///< Recent protocol messages reported by Moonlight callbacks. }; struct StreamInputThreadState { @@ -488,11 +578,7 @@ namespace { return; } - std::scoped_lock lock(connectionState->protocolLogMutex); - if (connectionState->recentProtocolMessages.size() >= MAX_CONNECTION_PROTOCOL_MESSAGES) { - connectionState->recentProtocolMessages.pop_front(); - } - connectionState->recentProtocolMessages.push_back(std::move(message)); + connectionState->protocolLog.append(std::move(message)); } /** @@ -502,8 +588,7 @@ namespace { * @return Latest protocol message, or an empty string when none was recorded. */ std::string latest_connection_protocol_message(const StreamConnectionState &connectionState) { - std::scoped_lock lock(connectionState.protocolLogMutex); - return connectionState.recentProtocolMessages.empty() ? std::string {} : connectionState.recentProtocolMessages.back(); + return connectionState.protocolLog.latest(); } /** @@ -526,8 +611,7 @@ namespace { connectionState->connectionTerminated.store(false); connectionState->poorConnection.store(false); connectionState->stopRequested.store(false); - std::scoped_lock lock(connectionState->protocolLogMutex); - connectionState->recentProtocolMessages.clear(); + connectionState->protocolLog.clear(); } std::string build_font_path() { @@ -547,9 +631,7 @@ namespace { resources->mediaBackend.shutdown(); { - std::scoped_lock lock(resources->controllerMutex); - close_controller(resources->controller); - resources->controller = nullptr; + resources->controller.close(); } if (resources->bodyFont != nullptr) { TTF_CloseFont(resources->bodyFont); @@ -620,7 +702,7 @@ namespace { return false; } - resources->controller = open_primary_controller(); + resources->controller.reset(open_primary_controller()); return true; } @@ -933,10 +1015,7 @@ namespace { return; } - std::scoped_lock lock(resources->controllerMutex); - if (resources->controller == nullptr) { - resources->controller = SDL_GameControllerOpen(deviceIndex); - } + resources->controller.open_if_needed(deviceIndex); } void close_controller_if_removed(StreamUiResources *resources, int joystickInstanceId) { @@ -944,16 +1023,7 @@ namespace { return; } - std::scoped_lock lock(resources->controllerMutex); - if (resources->controller == nullptr) { - return; - } - - SDL_Joystick *joystick = SDL_GameControllerGetJoystick(resources->controller); - if (joystick != nullptr && SDL_JoystickInstanceID(joystick) == joystickInstanceId) { - close_controller(resources->controller); - resources->controller = nullptr; - } + resources->controller.close_if_removed(joystickInstanceId); } SDL_ControllerDeviceEvent copy_controller_device_event(const SDL_Event &event) { @@ -1042,6 +1112,53 @@ namespace { return snapshot; } + void StreamControllerHandle::reset(SDL_GameController *controller) { + std::scoped_lock lock(mutex_); + controller_ = controller; + } + + void StreamControllerHandle::close() { + std::scoped_lock lock(mutex_); + close_controller(controller_); + controller_ = nullptr; + } + + void StreamControllerHandle::open_if_needed(int deviceIndex) { + std::scoped_lock lock(mutex_); + if (controller_ == nullptr) { + controller_ = SDL_GameControllerOpen(deviceIndex); + } + } + + void StreamControllerHandle::close_if_removed(int joystickInstanceId) { + std::scoped_lock lock(mutex_); + if (controller_ == nullptr) { + return; + } + + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller_); + if (joystick != nullptr && SDL_JoystickInstanceID(joystick) == joystickInstanceId) { + close_controller(controller_); + controller_ = nullptr; + } + } + + SDL_GameController *StreamControllerHandle::startup_poll_handle() const { + return controller_; + } + + ControllerSnapshot StreamControllerHandle::read_snapshot(bool *controllerPresent) const { + std::scoped_lock lock(mutex_); + if (controller_ == nullptr) { + return {}; + } + + if (controllerPresent != nullptr) { + *controllerPresent = true; + } + return read_controller_snapshot(controller_); + } + bool controller_snapshots_match(const ControllerSnapshot &left, const ControllerSnapshot &right) { return left.buttonFlags == right.buttonFlags && left.leftTrigger == right.leftTrigger && left.rightTrigger == right.rightTrigger && left.leftStickX == right.leftStickX && left.leftStickY == right.leftStickY && left.rightStickX == right.rightStickX && left.rightStickY == right.rightStickY; @@ -1092,7 +1209,7 @@ namespace { } } - ControllerSnapshot read_controller_snapshot(StreamUiResources *resources, bool *controllerPresent) { + ControllerSnapshot read_controller_snapshot(const StreamUiResources *resources, bool *controllerPresent) { if (controllerPresent != nullptr) { *controllerPresent = false; } @@ -1100,15 +1217,7 @@ namespace { return {}; } - std::scoped_lock lock(resources->controllerMutex); - if (resources->controller == nullptr) { - return {}; - } - - if (controllerPresent != nullptr) { - *controllerPresent = true; - } - return read_controller_snapshot(resources->controller); + return resources->controller.read_snapshot(controllerPresent); } void send_controller_snapshot_if_needed(const ControllerSnapshot &snapshot, bool controllerPresent, bool *arrivalSent, ControllerSnapshot *lastSnapshot) { @@ -1359,7 +1468,7 @@ namespace { Uint32 exitComboActivatedTick = 0U; while (!attempt.connectionState.startCompleted.load() && !attempt.connectionState.stopRequested.load()) { pump_stream_events(&attempt.resources); - update_stream_exit_combo(attempt.resources.controller, &exitComboActivatedTick, &attempt.connectionState); + update_stream_exit_combo(attempt.resources.controller.startup_poll_handle(), &exitComboActivatedTick, &attempt.connectionState); render_stream_frame(attempt.host, attempt.app, attempt.startContext, attempt.connectionState, &attempt.resources.mediaBackend, &attempt.resources); if (attempt.connectionState.stopRequested.load()) { LiInterruptConnection(); @@ -1408,7 +1517,7 @@ namespace { void poll_fallback_controller_if_needed( const SDL_Thread *inputThread, - StreamUiResources *resources, + const StreamUiResources *resources, StreamConnectionState *connectionState, Uint32 *exitComboActivatedTick, bool *controllerArrivalSent, diff --git a/src/ui/host_probe_result_queue.cpp b/src/ui/host_probe_result_queue.cpp index 9cee8db..65e9371 100644 --- a/src/ui/host_probe_result_queue.cpp +++ b/src/ui/host_probe_result_queue.cpp @@ -15,10 +15,10 @@ namespace ui { return; } - const std::scoped_lock lock(queue->mutex); - queue->targetCount = 0U; - queue->publishedCount = 0U; - queue->pendingResults.clear(); + const std::scoped_lock lock(queue->mutex_); + queue->targetCount_ = 0U; + queue->publishedCount_ = 0U; + queue->pendingResults_.clear(); } void begin_host_probe_result_round(HostProbeResultQueue *queue, std::size_t targetCount) { @@ -26,10 +26,10 @@ namespace ui { return; } - const std::scoped_lock lock(queue->mutex); - queue->targetCount = targetCount; - queue->publishedCount = 0U; - queue->pendingResults.clear(); + const std::scoped_lock lock(queue->mutex_); + queue->targetCount_ = targetCount; + queue->publishedCount_ = 0U; + queue->pendingResults_.clear(); } void publish_host_probe_result(HostProbeResultQueue *queue, HostProbeResult result) { @@ -37,9 +37,9 @@ namespace ui { return; } - const std::scoped_lock lock(queue->mutex); - queue->pendingResults.push_back(std::move(result)); - ++queue->publishedCount; + const std::scoped_lock lock(queue->mutex_); + queue->pendingResults_.push_back(std::move(result)); + ++queue->publishedCount_; } void skip_host_probe_result_target(HostProbeResultQueue *queue) { @@ -47,9 +47,9 @@ namespace ui { return; } - const std::scoped_lock lock(queue->mutex); - if (queue->targetCount > 0U) { - --queue->targetCount; + const std::scoped_lock lock(queue->mutex_); + if (queue->targetCount_ > 0U) { + --queue->targetCount_; } } @@ -58,15 +58,15 @@ namespace ui { return {}; } - const std::scoped_lock lock(queue->mutex); + const std::scoped_lock lock(queue->mutex_); std::vector results; - results.swap(queue->pendingResults); + results.swap(queue->pendingResults_); return results; } bool host_probe_result_round_complete(const HostProbeResultQueue &queue) { - const std::scoped_lock lock(queue.mutex); - return queue.targetCount != 0U && queue.publishedCount >= queue.targetCount; + const std::scoped_lock lock(queue.mutex_); + return queue.targetCount_ != 0U && queue.publishedCount_ >= queue.targetCount_; } } // namespace ui diff --git a/src/ui/host_probe_result_queue.h b/src/ui/host_probe_result_queue.h index be344b7..65a2385 100644 --- a/src/ui/host_probe_result_queue.h +++ b/src/ui/host_probe_result_queue.h @@ -29,11 +29,25 @@ namespace ui { /** * @brief Thread-safe queue used to publish per-host probe results back to the shell loop. */ - struct HostProbeResultQueue { - mutable std::mutex mutex; ///< Guards the current round counters and pending result queue. - std::size_t targetCount = 0U; ///< Number of results expected for the active probe round. - std::size_t publishedCount = 0U; ///< Number of results published so far for the active round. - std::vector pendingResults; ///< Probe results waiting to be drained by the main thread. + class HostProbeResultQueue { + public: + /** + * @brief Construct an empty probe result queue. + */ + HostProbeResultQueue() = default; + + private: + friend void reset_host_probe_result_queue(HostProbeResultQueue *queue); + friend void begin_host_probe_result_round(HostProbeResultQueue *queue, std::size_t targetCount); + friend void publish_host_probe_result(HostProbeResultQueue *queue, HostProbeResult result); + friend void skip_host_probe_result_target(HostProbeResultQueue *queue); + friend std::vector drain_host_probe_results(HostProbeResultQueue *queue); + friend bool host_probe_result_round_complete(const HostProbeResultQueue &queue); + + mutable std::mutex mutex_; ///< Guards the current round counters and pending result queue. + std::size_t targetCount_ = 0U; ///< Number of results expected for the active probe round. + std::size_t publishedCount_ = 0U; ///< Number of results published so far for the active round. + std::vector pendingResults_; ///< Probe results waiting to be drained by the main thread. }; /**