Files
Ange-Marie MAURIN - A2M Systèmes eb4db9e01f feat(render): processus de rendu lancé par le Core, scène simulée affichée dans Godot
ADR 0009 (Proposée) et spécification du protocole Core - rendu 0.1. Le Core projette le monde simulé (micro-mode 2D, SIMULÉ) en deltas SEQUA 3D (sequa::core_render) et les pousse après chaque tick qui change la scène (RenderHub, observateur de LiveSession) ; il lance et supervise sequa-render (RenderSupervisor : lié au Core, relance après plantage). sequa-render charge une bibliothèque d'adaptateur par un point d'entrée C versionné (3d/adapter_library.hpp), applique les deltas et rend les images ; codec JSON provisoire partagé dans 3d/wire. Adaptateur Godot en DLL et nouvel adaptateur de débogage (engines/debug/adapter : vue de côté en images PPM ou vidéo via ffmpeg externe). Protocole Studio - Core 0.3 : render/start, render/stop, render/status (Core et Studio host). Mesuré avec Godot : transport tick -> delta reçu p95 ≈ 0,3 ms (critère 4 ms).
2026-11-07 14:42:09 +01:00

137 lines
4.2 KiB
C++

// SPDX-License-Identifier: MPL-2.0
#include "sequa/debug/frame_sink.hpp"
#include <cstddef>
#include <fstream>
#include <system_error>
#include <utility>
namespace sequa::debug {
namespace {
// Une image doit correspondre à RGB8 complet : trois octets par pixel.
std::optional<three_d::Error> check_image(const Image& image)
{
const std::size_t expected =
static_cast<std::size_t>(image.width) * static_cast<std::size_t>(image.height) * 3;
if (image.rgb.size() != expected) {
return three_d::Error{three_d::Error::Code::invalid_argument, "taille d'image incohérente"};
}
return std::nullopt;
}
} // namespace
FrameSink::~FrameSink() = default;
// --- PpmSequenceSink ---
PpmSequenceSink::PpmSequenceSink(std::filesystem::path directory)
: directory_(std::move(directory))
{
}
std::optional<three_d::Error> PpmSequenceSink::write(const Image& image)
{
if (std::optional<three_d::Error> error = check_image(image)) {
return error;
}
std::error_code ec;
std::filesystem::create_directories(directory_, ec);
if (ec) {
return three_d::Error{three_d::Error::Code::engine_failure,
"création du dossier impossible : " + ec.message()};
}
char name[32];
std::snprintf(name, sizeof(name), "frame_%06llu.ppm",
static_cast<unsigned long long>(frame_index_));
const std::filesystem::path path = directory_ / name;
// En-tête PPM P6 : P6, dimensions, maximum.
const std::string header = "P6\n" + std::to_string(image.width) + ' '
+ std::to_string(image.height) + "\n255\n";
std::ofstream stream(path, std::ios::binary);
stream.write(header.data(), static_cast<std::streamsize>(header.size()));
stream.write(reinterpret_cast<const char*>(image.rgb.data()),
static_cast<std::streamsize>(image.rgb.size()));
stream.close();
if (!stream) {
return three_d::Error{three_d::Error::Code::engine_failure,
"écriture impossible : " + path.string()};
}
++frame_index_;
return std::nullopt;
}
// --- FfmpegSink ---
FfmpegSink::FfmpegSink(std::filesystem::path file, const std::uint32_t fps, std::string ffmpeg_path)
: file_(std::move(file)), fps_(fps), ffmpeg_path_(std::move(ffmpeg_path))
{
}
FfmpegSink::~FfmpegSink()
{
close();
}
std::optional<three_d::Error> FfmpegSink::write(const Image& image)
{
if (std::optional<three_d::Error> error = check_image(image)) {
return error;
}
if (pipe_ != nullptr && (image.width != width_ || image.height != height_)) {
return three_d::Error{three_d::Error::Code::engine_failure,
"taille d'image modifiée au cours de la vidéo"};
}
if (pipe_ == nullptr) {
// ffmpeg n'est jamais lié : simple outil externe, appelé en sous-processus.
const std::string command =
"\"" + ffmpeg_path_ + "\" -y -loglevel error -f rawvideo -pix_fmt rgb24 -s "
+ std::to_string(image.width) + "x" + std::to_string(image.height) + " -r "
+ std::to_string(fps_) + " -i - -c:v libx264 -pix_fmt yuv420p \"" + file_.string()
+ "\"";
#if defined(_WIN32)
pipe_ = ::_popen(command.c_str(), "wb");
#else
pipe_ = ::popen(command.c_str(), "w");
#endif
if (pipe_ == nullptr) {
return three_d::Error{three_d::Error::Code::engine_failure,
"lancement de ffmpeg impossible"};
}
width_ = image.width;
height_ = image.height;
}
if (image.rgb.empty()) {
return std::nullopt;
}
const std::size_t written = std::fwrite(image.rgb.data(), 1, image.rgb.size(), pipe_);
if (written != image.rgb.size()) {
return three_d::Error{three_d::Error::Code::engine_failure,
"écriture vers ffmpeg interrompue"};
}
return std::nullopt;
}
void FfmpegSink::close()
{
if (pipe_ == nullptr) {
return;
}
// Remis à nul d'abord : close() puis le destructeur ne ferment pas deux fois.
std::FILE* pipe = pipe_;
pipe_ = nullptr;
(void)std::fflush(pipe);
#if defined(_WIN32)
(void)::_pclose(pipe);
#else
(void)::pclose(pipe);
#endif
}
} // namespace sequa::debug