// SPDX-License-Identifier: MPL-2.0 #include "sequa/core/server/runtime.hpp" #include "sequa/core/server/protocol.hpp" #include "render_hub.hpp" #include "render_supervisor.hpp" #include #include #include #include #ifdef _WIN32 #include #else #include #endif namespace sequa::core::server { namespace { using SteadyClock = std::chrono::steady_clock; // 128 bits aléatoires en hexadécimal : identifie CETTE exécution du Core (spec §3). // Pas un secret : random_device suffit. std::string make_instance_id() { std::random_device device; std::string id; id.reserve(32); for (int i = 0; i < 4; ++i) { std::array chunk{}; std::snprintf(chunk.data(), chunk.size(), "%08x", static_cast(device())); id += chunk.data(); } return id; } std::int64_t current_pid() { #ifdef _WIN32 return static_cast(_getpid()); #else return static_cast(getpid()); #endif } LiveSession::Options session_options(const CoreRuntimeOptions& options) { LiveSession::Options session; session.seed = options.seed; session.real_time = options.real_time; return session; } // Convertit l'état interne du superviseur en RenderRunState du protocole. RenderRunState supervisor_state_to_protocol(RenderSupervisor::State state) { switch (state) { case RenderSupervisor::State::stopped: return RenderRunState::stopped; case RenderSupervisor::State::running: return RenderRunState::starting; // pas encore ready case RenderSupervisor::State::waiting_restart: return RenderRunState::restarting; } return RenderRunState::stopped; } // Construit la vue d'état complète pour le protocole. RenderStatusView build_render_status_view(const RenderSupervisor* supervisor, const RenderHub& hub) { RenderStatusView view; const RenderHub::Status hub_status = hub.status(); if (!supervisor) { view.state = RenderRunState::stopped; return view; } view.state = supervisor_state_to_protocol(supervisor->state()); if (view.state == RenderRunState::starting && hub_status.ready) { view.state = RenderRunState::running; // render/ready reçu } view.restarts = supervisor->restarts(); view.world_version = hub_status.world_version; view.last_failure = hub_status.last_failure.empty() ? std::nullopt : std::optional(hub_status.last_failure); if (supervisor->state() != RenderSupervisor::State::stopped) { view.pid = supervisor->pid(); } if (hub_status.ready && hub_status.capabilities) { view.engine = hub_status.capabilities->capabilities; view.width = hub_status.capabilities->width; view.height = hub_status.capabilities->height; } if (hub_status.frames) { view.measurements = *hub_status.frames; } return view; } } // namespace CoreRuntime::CoreRuntime(CoreRuntimeOptions options) : options_(std::move(options)), render_(std::make_unique(sim::ArenaParams{})), session_(session_options(options_)), started_at_(SteadyClock::now()), instance_id_(make_instance_id()), pid_(current_pid()) { // PROVISOIRE : la session utilise l'arène par défaut (LiveSession ne règle que graine et fréquence). session_.set_world_observer( [hub = render_.get()](const sim::WorldState& world, std::uint64_t tick) { hub->on_world(world, tick); }); if (options_.linked) { lease_active_ = true; attach_deadline_ = started_at_ + options_.attach_deadline; } } CoreRuntime::~CoreRuntime() = default; std::optional CoreRuntime::render_attach(ConnectionId id, PushFn push) { return render_->attach(id, std::move(push)); } void CoreRuntime::render_ready(ConnectionId id, const RenderReady& ready) { if (render_->ready(id, ready)) { session_.notify_world(); // scène complète tout de suite, même en pause } } void CoreRuntime::render_frames(ConnectionId id, const RenderFrames& frames) { render_->frames(id, frames); } void CoreRuntime::render_failed(ConnectionId id, const std::string& code, const std::string& message) { render_->failed(id, code, message); } void CoreRuntime::render_closed(ConnectionId id) { render_->closed(id); } CoreRuntime::ExitReason CoreRuntime::exit_reason() const { const std::lock_guard lock(mutex_); const auto now = SteadyClock::now(); if (shutdown_requested_ && now >= shutdown_at_) { return ExitReason::shutdown; } if (lease_active_ && !controller_) { if (grace_deadline_ && now >= *grace_deadline_) { return ExitReason::lease_expired; } if (attach_deadline_ && now >= *attach_deadline_) { return ExitReason::lease_expired; } } return ExitReason::none; } CoreStatus CoreRuntime::core_status() const { const std::lock_guard lock(mutex_); CoreStatus status; status.core_version = options_.core_version; status.protocol_version = std::string(protocol_version); status.instance_id = instance_id_; status.pid = pid_; status.uptime_ms = static_cast( std::chrono::duration_cast(SteadyClock::now() - started_at_).count()); status.linked = lease_active_; status.controller_attached = controller_.has_value(); status.clients = clients_; return status; } CoreServices::AttachResult CoreRuntime::attach(ConnectionId id, bool force) { const std::lock_guard lock(mutex_); if (controller_ && *controller_ != id && !force) { return AttachResult::controller_exists; } controller_ = id; lease_active_ = true; grace_deadline_.reset(); attach_deadline_.reset(); return AttachResult::attached; } CoreServices::DetachResult CoreRuntime::detach(ConnectionId id) { const std::lock_guard lock(mutex_); if (!controller_ || *controller_ != id) { return DetachResult::not_controller; } // La connexion reste contrôleur (un attach ultérieur rétablit le bail), mais sa // disparition n'arrêtera plus le Core. lease_active_ = false; grace_deadline_.reset(); attach_deadline_.reset(); return DetachResult::detached; } void CoreRuntime::request_shutdown() { const std::lock_guard lock(mutex_); if (!shutdown_requested_) { shutdown_requested_ = true; shutdown_at_ = SteadyClock::now() + options_.shutdown_delay; } } SessionStateView CoreRuntime::session_start(std::uint64_t seed) { return session_.restart(seed); } SessionStateView CoreRuntime::session_pause() { return session_.pause(); } SessionStateView CoreRuntime::session_resume() { return session_.resume(); } SessionStateView CoreRuntime::session_status() const { return session_.status(); } std::uint64_t CoreRuntime::session_input(std::uint32_t buttons, std::int32_t move_x, std::int32_t move_y) { return session_.set_input(buttons, move_x, move_y); } SessionExportData CoreRuntime::session_export() const { return session_.export_session(); } std::optional CoreRuntime::session_import(std::span recording, bool resume) { return session_.import_session(recording, resume); } // --- Rendu piloté par Studio (protocole 0.3, §9 ; ADR 0010) --- RenderStartResult CoreRuntime::render_start(const RenderStartRequest& request) { // Verrou dédié au superviseur (pas mutex_ pour éviter les inversions avec LiveSession). const std::lock_guard lock(supervisor_mutex_); // Déjà un superviseur actif (pas stopped) ? if (supervisor_ && supervisor_->state() != RenderSupervisor::State::stopped) { return RenderStartResult{build_render_status_view(supervisor_.get(), *render_), "render-running", "Un rendu est déjà en cours."}; } // Vérifier l'adaptateur. if (request.adapter != "godot" && request.adapter != "debug") { return RenderStartResult{std::nullopt, "render-unavailable", "Adaptateur inconnu : " + request.adapter}; } // Trouver l'exécutable et la bibliothèque d'adaptateur à partir de la racine SEQUA. if (options_.sequa_root.empty()) { return RenderStartResult{std::nullopt, "render-unavailable", "Racine SEQUA introuvable (sequa-root.json) : lancer depuis bin/// ou passer --sequa-root."}; } const auto executable = find_render_executable(options_.sequa_root); const auto adapter_lib = find_adapter_library(options_.sequa_root, request.adapter); if (!executable || !adapter_lib) { std::string msg = "Rendu introuvable : "; if (!executable) msg += "sequa-render "; if (!adapter_lib) msg += "bibliothèque d'adaptateur " + request.adapter + " "; msg += "absente."; return RenderStartResult{std::nullopt, "render-unavailable", std::move(msg)}; } // Préparer le lancement. RenderLaunch launch; launch.executable = *executable; launch.adapter = *adapter_lib; launch.width = request.width; launch.height = request.height; launch.headless = request.headless; launch.fps = request.fps; launch.adapter_options = request.adapter_options; launch.log_path = options_.render_log; // Créer et lancer le superviseur. supervisor_ = std::make_unique(std::move(launch), options_.render_core_url, options_.log); if (!supervisor_->start()) { supervisor_.reset(); return RenderStartResult{std::nullopt, "render-unavailable", "Lancement du superviseur de rendu échoué."}; } return RenderStartResult{build_render_status_view(supervisor_.get(), *render_), "", ""}; } RenderStatusView CoreRuntime::render_stop() { const std::lock_guard lock(supervisor_mutex_); if (!supervisor_) { return RenderStatusView{}; // state = stopped } // Bloque jusqu'à ~5 s (render/stop, attente 3 s, terminaison forcée) en gardant le verrou du // superviseur : tick_render() attend pendant ce temps, ce qui évite de relancer un rendu // qu'on est en train d'arrêter. Ordre des verrous : superviseur puis canal de rendu. RenderStatusView view = build_render_status_view(supervisor_.get(), *render_); supervisor_->stop(*render_); supervisor_.reset(); view.state = RenderRunState::stopped; view.adapter.reset(); view.pid.reset(); view.engine.reset(); view.width.reset(); view.height.reset(); view.measurements.reset(); // world_version et last_failure restent : utiles pour savoir où en était le rendu. return view; } RenderStatusView CoreRuntime::render_status() const { const std::lock_guard lock(supervisor_mutex_); if (!supervisor_) { return RenderStatusView{}; // state = stopped } return build_render_status_view(supervisor_.get(), *render_); } void CoreRuntime::tick_render() { const std::lock_guard lock(supervisor_mutex_); if (supervisor_) { supervisor_->tick(); } } void CoreRuntime::stop_render() { const std::lock_guard lock(supervisor_mutex_); if (supervisor_) { supervisor_->stop(*render_); supervisor_.reset(); } } void CoreRuntime::on_client_connected() { const std::lock_guard lock(mutex_); ++clients_; } void CoreRuntime::on_client_disconnected(ConnectionId id) { render_->disconnected(id); // avant que la socket ne disparaisse (fonction d'envoi oubliée) const std::lock_guard lock(mutex_); if (clients_ > 0) { --clients_; } if (controller_ && *controller_ == id) { controller_.reset(); if (lease_active_) { grace_deadline_ = SteadyClock::now() + options_.lease_grace; } } } } // namespace sequa::core::server