Files
SEQUA-Engine/core/src/server/runtime.cpp
T
Ange-Marie MAURIN - A2M Systèmes da0d31385b build: arborescence bin/<plateforme>/<type> et build unifié (ADR 0010)
- platform/layout : racine sequa-root.json, chemins des composants, arguments et chemins en UTF-8.
- Règles install() : bin/windows-x64/{debug,release,asan}/ (core, render, studio + ui, engines).
- Le Core trouve sequa-render et les adaptateurs depuis la racine (--sequa-root, passé par
  Studio host à sa copie fantôme) : plus de render-unavailable depuis le panneau Rendu.
- L'adaptateur Godot cherche Godot et son projet à côté de lui (surcharge par --adapter-options) ;
  template_release pour le type release. Godot hébergé dans les presets debug et release.
- scripts/build-all.ps1 : Godot, C++, interface, arrêt des processus du dossier visé, install ;
  check.ps1 l'appelle (modification de l'ADR 0004).
- docs/installation/ : compiler, lancer, installer et désinstaller par plateforme.
2026-11-07 15:50:37 +01:00

387 lines
12 KiB
C++

// 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 <array>
#include <cstdio>
#include <random>
#include <utility>
#ifdef _WIN32
#include <process.h>
#else
#include <unistd.h>
#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<char, 9> chunk{};
std::snprintf(chunk.data(), chunk.size(), "%08x", static_cast<unsigned>(device()));
id += chunk.data();
}
return id;
}
std::int64_t current_pid()
{
#ifdef _WIN32
return static_cast<std::int64_t>(_getpid());
#else
return static_cast<std::int64_t>(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<std::string>(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<RenderHub>(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<std::string> 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::uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(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<SessionImportData> CoreRuntime::session_import(std::span<const std::byte> 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/<plateforme>/<type>/ 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<RenderSupervisor>(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