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).
137 lines
4.2 KiB
C++
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
|