Files
SEQUA-Engine/core/tests/render/test_scene_projection.cpp
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

288 lines
10 KiB
C++

// SPDX-License-Identifier: MPL-2.0
#include "sequa/core/render/scene_projection.hpp"
#include "sequa/core/sim/simulation.hpp"
#include <doctest/doctest.h>
#include <cstddef>
#include <cstdint>
#include <variant>
#include <vector>
using namespace sequa::core::sim;
using sequa::core::render::SceneProjector;
using namespace sequa::three_d;
namespace {
constexpr ObjectId ground_id = 1;
constexpr ObjectId player_id = 2;
constexpr ObjectId coin_id_base = 1000;
// Monde de test deterministe : aucun corpus aleatoire a traiter, tout est pose a la main.
WorldState world_with_three_coins()
{
WorldState world;
world.next_coin_id = 3;
world.coins.push_back(Coin{0, Fixed::from_int(2), Fixed::from_ratio(1, 2)});
world.coins.push_back(Coin{1, Fixed::from_int(-4), Fixed::from_int(1)});
world.coins.push_back(Coin{2, Fixed::from_int(10), Fixed::from_int(2)});
return world;
}
// Session scriptee : courses, pauses, demi-tours et sauts periodiques suffisent a faire
// bouger joueur, pieces et camera. Copie locale des sessions des tests sim (meme idee).
InputFrame scripted_input(Tick tick)
{
InputFrame input = neutral_input(tick);
const Tick phase = tick % 180;
if (phase < 90) {
input.move_x = 32767;
} else if (phase < 120) {
input.move_x = 0;
} else {
input.move_x = -12000;
}
if (tick % 60 < 4) {
input.buttons = button::jump;
}
return input;
}
template <typename Command>
[[nodiscard]] std::size_t count_commands(const SceneDelta& delta)
{
std::size_t count = 0;
for (const SceneCommand& command : delta.commands) {
if (std::holds_alternative<Command>(command)) {
++count;
}
}
return count;
}
template <typename Command>
[[nodiscard]] const Command* find_command(const SceneDelta& delta, ObjectId id)
{
for (const SceneCommand& command : delta.commands) {
if (const Command* c = std::get_if<Command>(&command); c != nullptr && c->id == id) {
return c;
}
}
return nullptr;
}
void check_no_set_visual(const SceneDelta& delta)
{
CHECK(count_commands<SetVisual>(delta) == 0);
}
} // namespace
TEST_CASE("SceneProjector : le premier appel renvoie la scene complete")
{
SceneProjector projector(ArenaParams{});
const SceneDelta delta = projector.update(world_with_three_coins(), 5);
check_no_set_visual(delta);
CHECK(delta.world_version == 5);
REQUIRE(delta.commands.size() == 6); // sol, joueur, 3 pieces, camera
// Les creations arrivent d'abord, par identifiant croissant.
std::vector<ObjectId> created;
for (const SceneCommand& command : delta.commands) {
if (const CreateObject* c = std::get_if<CreateObject>(&command)) {
created.push_back(c->id);
}
}
CHECK(created == std::vector<ObjectId>{ground_id, player_id, 1000, 1001, 1002});
CHECK(std::holds_alternative<SetCamera>(delta.commands.back()));
const CreateObject* ground = find_command<CreateObject>(delta, ground_id);
REQUIRE(ground != nullptr);
CHECK(ground->visual.primitive == Primitive::plane);
CHECK(ground->visual.size == Vec3{40, 1, 4}); // 2 * half_width par defaut
CHECK(ground->transform.position == Vec3{0, 0, 0});
CHECK(ground->visual.color.r < 0.5f); // vert sombre : dominante verte
CHECK(ground->visual.color.g > ground->visual.color.r);
CHECK(ground->visual.color.g > ground->visual.color.b);
const CreateObject* player = find_command<CreateObject>(delta, player_id);
REQUIRE(player != nullptr);
CHECK(player->visual.primitive == Primitive::capsule);
CHECK(player->visual.size == Vec3{0.8f, 1.8f, 0.8f});
// Body.y est l'altitude des pieds : le centre de la capsule est une demi-hauteur plus
// haut, donc a y = 0.9 au sol.
CHECK(player->transform.position == Vec3{0, 0.9f, 0});
const CreateObject* coin = find_command<CreateObject>(delta, coin_id_base);
REQUIRE(coin != nullptr);
CHECK(coin->visual.primitive == Primitive::sphere);
CHECK(coin->visual.size == Vec3{0.5f, 0.5f, 0.5f}); // premiere piece posee a (2, 0.5)
CHECK(coin->transform.position == Vec3{2, 0.5f, 0});
// SetCamera n'a pas d'identifiant : la camera ferme la scene complete.
const SetCamera* camera_command = std::get_if<SetCamera>(&delta.commands.back());
REQUIRE(camera_command != nullptr);
CHECK(camera_command->camera.transform.position == Vec3{0, 3, 12});
CHECK(camera_command->camera.transform.rotation == Quat{0, 0, 0, 1});
CHECK(camera_command->camera.vertical_fov_deg == 50.0f);
CHECK(camera_command->camera.near_plane == 0.1f);
CHECK(camera_command->camera.far_plane == 200.0f);
}
TEST_CASE("SceneProjector : rien n'a bouge, delta vide")
{
SceneProjector projector(ArenaParams{});
const WorldState world = world_with_three_coins();
REQUIRE(projector.update(world, 1).commands.size() == 6);
const SceneDelta delta = projector.update(world, 2);
CHECK(delta.world_version == 2);
CHECK(delta.commands.empty());
}
TEST_CASE("SceneProjector : seul le joueur bouge, exactement ce qui a change")
{
SceneProjector projector(ArenaParams{});
WorldState world = world_with_three_coins();
REQUIRE(projector.update(world, 1).commands.size() == 6);
SUBCASE("deplacement horizontal : transform et camera")
{
world.player.x = Fixed::from_int(1);
const SceneDelta delta = projector.update(world, 2);
check_no_set_visual(delta);
CHECK(count_commands<SetTransform>(delta) == 1);
const SetTransform* transform = find_command<SetTransform>(delta, player_id);
REQUIRE(transform != nullptr);
CHECK(transform->transform.position == Vec3{1, 0.9f, 0});
CHECK(count_commands<SetCamera>(delta) == 1);
CHECK(count_commands<CreateObject>(delta) == 0);
CHECK(count_commands<DestroyObject>(delta) == 0);
}
SUBCASE("montee verticale seule : transform sans camera")
{
world.player.x = Fixed::from_int(1);
REQUIRE(projector.update(world, 2).commands.size() == 2);
world.player.y = Fixed::from_ratio(1, 2);
const SceneDelta delta = projector.update(world, 3);
check_no_set_visual(delta);
CHECK(count_commands<SetTransform>(delta) == 1);
const SetTransform* transform = find_command<SetTransform>(delta, player_id);
REQUIRE(transform != nullptr);
CHECK(transform->transform.position == Vec3{1, 1.4f, 0}); // pieds a y = 0.5
CHECK(count_commands<SetCamera>(delta) == 0);
}
}
TEST_CASE("SceneProjector : une piece ramassee devient un DestroyObject")
{
SceneProjector projector(ArenaParams{});
WorldState world = world_with_three_coins();
REQUIRE(projector.update(world, 1).commands.size() == 6);
// La piece d'identifiant 1 a ete ramassee : elle quitte le monde.
world.coins.erase(world.coins.begin() + 1);
const SceneDelta delta = projector.update(world, 2);
check_no_set_visual(delta);
REQUIRE(delta.commands.size() == 1);
const DestroyObject destroyed = std::get<DestroyObject>(delta.commands.front());
CHECK(destroyed.id == 1001);
}
TEST_CASE("SceneProjector : une piece apparue devient un CreateObject")
{
SceneProjector projector(ArenaParams{});
WorldState world = world_with_three_coins();
REQUIRE(projector.update(world, 1).commands.size() == 6);
// La piece ramassee reapparait ailleurs sous un nouvel identifiant.
world.coins.push_back(Coin{5, Fixed::from_int(6), Fixed::from_int(1)});
const SceneDelta delta = projector.update(world, 2);
check_no_set_visual(delta);
REQUIRE(delta.commands.size() == 1);
const CreateObject created = std::get<CreateObject>(delta.commands.front());
CHECK(created.id == 1005);
CHECK(created.visual.primitive == Primitive::sphere);
CHECK(created.transform.position == Vec3{6, 1, 0});
}
TEST_CASE("SceneProjector : reset() renvoie a la scene complete")
{
SceneProjector projector(ArenaParams{});
WorldState world = world_with_three_coins();
REQUIRE(projector.update(world, 1).commands.size() == 6);
world.player.x = Fixed::from_int(1);
REQUIRE(projector.update(world, 2).commands.size() == 2);
projector.reset();
const SceneDelta delta = projector.update(world, 3);
check_no_set_visual(delta);
// Toute la scene est repoussee, transform du joueur compris.
CHECK(delta.commands.size() == 6);
CHECK(count_commands<SetTransform>(delta) == 0);
CHECK(count_commands<SetCamera>(delta) == 1);
}
TEST_CASE("SceneProjector : une vraie session passee pas a pas est acceptee par SceneState")
{
SimConfig config;
config.seed = 1234;
Simulation simulation(config);
SceneProjector projector(simulation.config().arena);
SceneState scene;
std::size_t non_empty_deltas = 0;
for (Tick tick = 0; tick < 300; ++tick) {
const SceneDelta delta = projector.update(simulation.world(), tick + 1);
REQUIRE_FALSE(scene.apply(delta).has_value());
if (!delta.commands.empty()) {
++non_empty_deltas;
}
for (const SceneCommand& command : delta.commands) {
CHECK_FALSE(std::holds_alternative<SetVisual>(command));
}
simulation.step(scripted_input(tick));
}
CHECK(scene.world_version() == 300);
CHECK(scene.objects().size() == 2 + simulation.config().arena.coin_count); // sol, joueur, pieces
CHECK(scene.camera().has_value());
CHECK(non_empty_deltas > 0); // la session a bien fait bouger le joueur
}
TEST_CASE("SceneProjector : meme suite d'entrees, memes deltas")
{
SimConfig config;
config.seed = 1234;
Simulation a(config);
Simulation b(config);
SceneProjector projector_a(a.config().arena);
SceneProjector projector_b(b.config().arena);
for (Tick tick = 0; tick < 300; ++tick) {
const SceneDelta delta_a = projector_a.update(a.world(), tick + 1);
const SceneDelta delta_b = projector_b.update(b.world(), tick + 1);
CHECK(delta_a.world_version == delta_b.world_version);
CHECK(delta_a.commands == delta_b.commands);
const InputFrame input = scripted_input(tick);
a.step(input);
b.step(input);
}
}