Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/editor/play.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ splash::ExportResult exportForPlay(const splash::Scene& scene, const fs::path& p
}
// Stale files from an earlier play must not be booted if this export fails.
for (const char* ext : {".splashpack", ".vram", ".spu"}) fs::remove(dir / (std::string("scene_0") + ext), ec);
fs::remove(dir / "reload.flag", ec);
splash::ExportOptions opt;
opt.luaBytecode = true;
try {
Expand All @@ -168,6 +169,36 @@ bool parseRenderPeak(const std::string& line, RenderPeak* out) {
return true;
}

bool writeReloadFlag(const fs::path& dir, unsigned generation) {
const fs::path tmp = dir / "reload.flag.tmp";
{
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
out << generation;
if (!out) return false;
}
std::error_code ec;
fs::rename(tmp, dir / "reload.flag", ec);
return !ec;
}

splash::ExportResult reloadForPlay(const splash::Scene& scene, const fs::path& projectRoot, const fs::path& dir,
unsigned generation) {
const fs::path next = dir / "next";
std::error_code ec;
fs::remove_all(next, ec);
splash::ExportResult r = exportForPlay(scene, projectRoot, next);
if (!r.ok()) return r;
for (const fs::directory_entry& e : fs::directory_iterator(next, ec)) {
fs::rename(e.path(), dir / e.path().filename(), ec);
if (ec) {
r.errors.push_back("Cannot replace " + utf8(dir / e.path().filename()) + ": " + ec.message());
return r;
}
}
if (!writeReloadFlag(dir, generation)) r.errors.push_back("Cannot write " + utf8(dir / "reload.flag"));
return r;
}

std::vector<std::string> reduxCommand(const PlayTools& t, const fs::path& dir) {
std::vector<std::string> a = {utf8(t.redux), "-run", "-fastboot", "-no-ui", "-shmdisplay"};
if (!t.bios.empty()) {
Expand Down
9 changes: 9 additions & 0 deletions src/editor/play.hh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ std::vector<std::string> missingHardwareTools(const PlayTools& tools);
splash::ExportResult exportForPlay(const splash::Scene& scene, const std::filesystem::path& projectRoot,
const std::filesystem::path& dir);

// Live edits while the game runs. psxsplash re-reads dir/reload.flag every
// few frames when it existed at boot, and reloads the scene in place when its
// contents change. writeReloadFlag stores `generation` there; reloadForPlay
// exports into dir/next, moves the files over the ones the game loads, then
// bumps the flag. A failed export leaves the running scene's files alone.
bool writeReloadFlag(const std::filesystem::path& dir, unsigned generation);
splash::ExportResult reloadForPlay(const splash::Scene& scene, const std::filesystem::path& projectRoot,
const std::filesystem::path& dir, unsigned generation);

// What a development (PCdrv) psxsplash reports using, from its line
// "psxsplash: render peak depth D of N, bump B of M".
struct RenderPeak {
Expand Down
19 changes: 19 additions & 0 deletions src/editor/tests/editor_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,25 @@ void testPlay() {
CHECK(!b.ok());
CHECK(!fs::exists(dir / "scene_0.splashpack") && !fs::exists(dir / "scene_0.vram") && !fs::exists(dir / "scene_0.spu"));

// Live edits: a good export replaces the files and bumps the flag the game
// polls; a bad one leaves both as they were. Play writes the flag at 0.
auto flag = [&] {
std::ifstream in(dir / "reload.flag");
std::string v;
std::getline(in, v);
return v;
};
CHECK(editor::exportForPlay(scene, project, dir).ok() && !fs::exists(dir / "reload.flag"));
CHECK(editor::writeReloadFlag(dir, 0) && flag() == "0");
splash::Scene moved = scene;
moved.objects.push_back(named("added during play"));
splash::ExportResult lr = editor::reloadForPlay(moved, project, dir, 3);
CHECK(lr.ok() && flag() == "3");
CHECK(fs::file_size(dir / "scene_0.splashpack", ec) == lr.stats.splashpackBytes);
const auto packTime = fs::last_write_time(dir / "scene_0.splashpack", ec);
CHECK(!editor::reloadForPlay(bad, project, dir, 4).ok());
CHECK(flag() == "3" && fs::exists(dir / "scene_0.vram") && fs::last_write_time(dir / "scene_0.splashpack", ec) == packTime);

// Settings: round trip (non-ASCII path), missing file, unknown keys.
const fs::path cfg = g_outDir / "play" / "play.cfg";
editor::PlayTools t;
Expand Down
36 changes: 33 additions & 3 deletions src/mockup/main_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ static void togglePlay(State& st, const editor::Document& doc) {
return;
}
if (p.build.valid()) return;
if (p.reload.valid()) p.reload.wait(); // a live edit still writing the folder
p.message.clear();
p.peak.reset();
p.peakScene = doc.loadId();
Expand All @@ -118,8 +119,13 @@ static void togglePlay(State& st, const editor::Document& doc) {
p.setupFor = 0;
return;
}
p.sentRev = p.seenRev = doc.revision();
p.reloadGeneration = 0;
p.build = std::async(std::launch::async, [scene = doc.scene(), root = doc.projectRoot()] {
return editor::exportForPlay(scene, root, playDir());
splash::ExportResult r = editor::exportForPlay(scene, root, playDir());
// Its presence at boot is what makes the game watch for live edits.
if (r.ok() && !editor::writeReloadFlag(playDir(), 0)) r.errors.push_back("Cannot write reload.flag");
return r;
});
}

Expand Down Expand Up @@ -153,8 +159,32 @@ static void toggleRun(State& st, const editor::Document& doc) {
// Ctrl+B and the Export button: ask where the disc image goes, then build it.
static void startExport(State& st, const editor::Document& doc);

static void updatePlay(State& st) {
// Edits made while the game runs: once the document has stayed unchanged for
// a moment, export it again and tell the running game to reload the scene.
static void sendEdits(State& st, const editor::Document& doc) {
State::Play& p = st.play;
const double now = ImGui::GetTime();
if (p.reload.valid()) {
if (p.reload.wait_for(std::chrono::seconds(0)) != std::future_status::ready) return;
splash::ExportResult r = p.reload.get();
p.message = r.ok() ? std::string() : "Live edit not sent: " + r.errors.front();
}
if (!p.emu.running() || p.build.valid() || doc.loadId() != p.peakScene) return;
if (doc.revision() != p.seenRev) {
p.seenRev = doc.revision();
p.seenAt = now;
}
if (doc.revision() == p.sentRev || now - p.seenAt < 0.3) return;
p.sentRev = doc.revision();
const unsigned gen = ++p.reloadGeneration;
p.reload = std::async(std::launch::async, [scene = doc.scene(), root = doc.projectRoot(), gen] {
return editor::reloadForPlay(scene, root, playDir(), gen);
});
}

static void updatePlay(State& st, const editor::Document& doc) {
State::Play& p = st.play;
sendEdits(st, doc);
if (p.hwBuild.valid() && p.hwBuild.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
splash::ExportResult r = p.hwBuild.get();
const editor::PlayTools eff = editor::withDefaults(p.tools, p.bundleDir);
Expand Down Expand Up @@ -3597,7 +3627,7 @@ ImRect drawMainScreen(State& st, editor::Document& doc, viewport::Ps1View& view,
filesPending(st, doc);
shortcuts(st, doc);
st.live.update(doc, ImGui::GetTime());
updatePlay(st);
updatePlay(st, doc);
updateExport(st);
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(size);
Expand Down
6 changes: 6 additions & 0 deletions src/mockup/main_screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ struct State {
std::optional<editor::RenderPeak> peak;
unsigned peakScene = 0;
uint64_t linesSeen = 0;
// Live edits: the document revision the running game was last sent,
// the revision seen last frame and since when, the export in flight,
// and the count written to reload.flag.
std::future<splash::ExportResult> reload;
unsigned sentRev = 0, seenRev = 0, reloadGeneration = 0;
double seenAt = 0;
} play;

// F2 rename in the tree: the object's path, and whether the editor still has to open.
Expand Down
Loading