- 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.
387 lines
12 KiB
C++
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
|