Skip to content

Commit ba727eb

Browse files
committed
feat: incremental hot-reload (stack diffing + resetModifications) + visual regression suite
Restructures Context.stack from a flat list into a nested dict (stack[inputIdx][frameIdx][shaderKey]), which gives Apply entries natural O(1) dedup and a stable address for diffing — Wait/Timestamp move to a separate Context.events list since C++ doesn't need them in the stack. Core::reloadSourceFile now diffs the freshly-executed stack against a snapshot of the previous one (Core::_stackSnapshot): inputs that didn't change at all are skipped entirely (mesh/GPU caches + texture descriptors stay valid), and inputs whose Create entry is unchanged but whose modifications changed reuse the existing object via the new AInput::resetModifications() instead of re-running their constructor — avoiding redundant Image/Video file I/O on every edit-and-reload cycle. Full reconstruction only happens when Create args changed or the input set itself changed (Python's sequential index counter gets reshuffled by add/remove, so positional diffing isn't safe there). Wires 'R' in the preview window to trigger the reload + texture re-upload. Adds a --visual-test suite (make test / make test-golden) that renders scenes headlessly via VulkanHeadlessRenderer and checks two things: golden-frame regression (rendered frames vs. stored PNGs in test/visual/golden/) and hot-reload equivalence (does an incremental reload produce pixel-identical output to a fresh load of the same scene?) — exercising both the "skip unchanged" and "reuse + replay" paths above. Test scenes live in test/visual/scenes/, including a reload_a/reload_b pair built specifically to cover both reuse paths.
1 parent 9ad1f3b commit ba727eb

29 files changed

Lines changed: 643 additions & 104 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,9 @@ wherewasi
5454
todo
5555
pgn
5656

57+
# Visual regression golden images — tracked despite the *.png rule above,
58+
# the test suite needs them to compare against on a fresh checkout.
59+
!test/visual/golden/*.png
60+
5761
# WebImage
5862
webimage/

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ set(SOURCES
123123
# --- Compiler ---
124124
src/compiler/Compiler.cpp
125125

126+
# --- Test ---
127+
src/test/VisualTest.cpp
128+
126129
# --- Core ---
127130
src/core/Core.cpp
128131

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ docvid:
8080
./$(BINARY_NAME) --generate
8181

8282

83+
# Visual regression suite — golden-frame + hot-reload equivalence checks.
84+
.PHONY: test
85+
test:
86+
./$(BINARY_NAME) --visual-test
87+
88+
89+
# (Re)write the golden images the suite compares against.
90+
.PHONY: test-golden
91+
test-golden:
92+
./$(BINARY_NAME) --visual-test --update-golden
93+
94+
8395
# 1. Generate the Readme
8496
# 2. Copies the generated video to example.gif
8597
.PHONY: docdoc

include/core/Core.hpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,22 @@ namespace VC
6868

6969
private:
7070

71-
void executeStack(const py::list& stack);
71+
void executeStack(const py::dict& stack, const py::list& events);
72+
73+
///< (Re)build a single input from its stack subtree (Create + Apply entries),
74+
///< replacing _inputs[idx] in place. Used by both full and incremental rebuilds.
75+
///< When reuseExisting is true and _inputs[idx]'s Create entry is unchanged, the
76+
///< existing AInput is kept alive (skipping its — possibly expensive — constructor,
77+
///< e.g. Image/Video file I/O) and only its modification state is reset + replayed.
78+
void rebuildInput(size_t idx, const py::dict& inputData, bool reuseExisting);
79+
80+
///< Per-input snapshot of the stack (as JSON) from the last reload — diffed against
81+
///< the freshly-executed stack so reloadSourceFile() only rebuilds inputs that changed.
82+
std::map<int, json> _stackSnapshot{};
83+
84+
///< Indices of inputs rebuilt during the last executeStack() that need a texture
85+
///< (re)upload — consumed and cleared by uploadTextures().
86+
std::vector<size_t> _pendingTextureUpload{};
7287

7388
///< Config (Window / Framerate / Paths)
7489
const Config& _config;

include/input/AInput.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ class AInput : public IInput
3131

3232
// -
3333

34+
// Restores all modification-derived state (metas, effects, timeline) to the
35+
// freshly-constructed state, keeping _baseArgs (and any subclass resources, e.g.
36+
// a loaded texture or open video file). Lets hot-reload reuse an input whose
37+
// Create entry didn't change without re-running its (possibly expensive) constructor.
38+
void resetModifications();
39+
40+
// -
41+
3442
Metadata getMetadata(size_t index) final;
3543

3644
// -

include/test/VisualTest.hpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
** EPITECH PROJECT, 2026
3+
** video-code
4+
** File description:
5+
** VisualTest — golden-frame & hot-reload visual regression suite
6+
*/
7+
8+
#pragma once
9+
10+
#include <argparse/argparse.hpp>
11+
#include <opencv2/opencv.hpp>
12+
#include <string>
13+
#include <vector>
14+
15+
#include "core/Config.hpp"
16+
17+
namespace VC
18+
{
19+
// -------------------------------------------------------------------------
20+
// VisualTest
21+
// Renders a fixed set of scenes headlessly (VulkanHeadlessRenderer, no
22+
// ffmpeg) and compares the resulting frames against golden PNGs stored in
23+
// test/visual/golden/. Also checks that hot-reloading from one scene to
24+
// another (Core::reloadSourceFile) produces pixel-identical output to a
25+
// fresh load of the destination scene — the "reload equivalence" check.
26+
//
27+
// Run with `--visual-test`; pass `--update-golden` to (re)write the
28+
// golden images instead of comparing against them.
29+
// -------------------------------------------------------------------------
30+
class VisualTest
31+
{
32+
public:
33+
34+
explicit VisualTest(const argparse::ArgumentParser& parser);
35+
36+
// Runs every registered case, printing PASS/FAIL per check.
37+
// Returns the number of failed checks (0 = everything passed).
38+
int run(bool updateGolden);
39+
40+
private:
41+
42+
std::vector<cv::Mat> renderFrames(const std::string& scenePath, const std::vector<size_t>& frames);
43+
std::vector<cv::Mat> renderFramesAfterReload(
44+
const std::string& before, const std::string& after, const std::vector<size_t>& frames);
45+
46+
const argparse::ArgumentParser& _parser;
47+
Config _baseConfig;
48+
};
49+
};

include/utils/Logger.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ namespace VC
4444
constexpr const char* CYAN = "\033[36m";
4545
constexpr const char* RED = "\033[31m";
4646
constexpr const char* GREEN = "\033[32m";
47+
constexpr const char* YELLOW = "\033[33m";
4748
constexpr const char* MAGENTA = "\033[35m";
4849
constexpr const char* RESET = "\033[0m";
4950
}

src/Main.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <opencv2/opencv.hpp>
1818

1919
#include "compiler/Compiler.hpp"
20+
#include "test/VisualTest.hpp"
2021
#include "window/Window.hpp"
2122

2223
namespace py = pybind11;
@@ -64,6 +65,16 @@ void setParserArgument(argparse::ArgumentParser &p)
6465
.add_argument("--showtimeline")
6566
.flag()
6667
.help("Show the timeline of the video.");
68+
69+
p
70+
.add_argument("--visual-test")
71+
.flag()
72+
.help("Run the visual regression suite (golden-frame + hot-reload equivalence checks) and exit.");
73+
74+
p
75+
.add_argument("--update-golden")
76+
.flag()
77+
.help("With --visual-test, (re)write the golden images instead of comparing against them.");
6778
}
6879

6980
int main(int argc, char *argv[])
@@ -98,6 +109,12 @@ int main(int argc, char *argv[])
98109
return EXIT_FAILURE;
99110
}
100111

112+
// Visual regression suite (headless — no window, no Qt event loop)
113+
if (parser.get<bool>("--visual-test")) {
114+
VC::VisualTest visualTest(parser);
115+
return visualTest.run(parser.get<bool>("--update-golden"));
116+
}
117+
101118
// Generate the video (headless — no window, no Qt event loop)
102119
if (parser.is_used("--generate")) {
103120
VC::Compiler compiler(parser);

0 commit comments

Comments
 (0)