Skip to content

Commit 4656482

Browse files
committed
Use zips for projects
1 parent 8299545 commit 4656482

22 files changed

Lines changed: 696 additions & 256 deletions

CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ add_executable(rpc-schema-dump
225225
src/system/mesen/GbaSystem.cpp
226226
src/system/mesen/roles/NesN8MidiRole.cpp)
227227
target_include_directories(rpc-schema-dump PRIVATE src)
228-
target_link_libraries(rpc-schema-dump PRIVATE rpcpp sameboy mesen r8brain enkiTS miniaudio efsw)
228+
target_include_directories(rpc-schema-dump PRIVATE deps/lv_binding_js/deps/txiki/deps/miniz)
229+
target_link_libraries(rpc-schema-dump PRIVATE rpcpp sameboy mesen r8brain enkiTS miniaudio efsw miniz)
229230

230231
# --- UI bundle: esbuild-bundled TS/TSX, embedded into the plugin as a byte array ---
231232
find_program(NODE_EXECUTABLE node REQUIRED)
@@ -379,6 +380,11 @@ target_link_libraries(${NAME} PUBLIC miniaudio)
379380
# PluginRpcService.cpp (shared between FILES_DSP and FILES_UI) references
380381
# UserConfig symbols and we don't split the .cpp.
381382
target_link_libraries(${NAME} PUBLIC efsw)
383+
# Project files are PKZIP blobs (project.json + raw binary entries); miniz
384+
# provides the in-memory writer/reader. The target is built as part of txiki
385+
# but we link + include directly so src/util/MinizZip.hpp can #include "miniz.h".
386+
target_link_libraries(${NAME} PUBLIC miniz)
387+
target_include_directories(${NAME} PUBLIC deps/lv_binding_js/deps/txiki/deps/miniz)
382388

383389
# Force UI artifacts to refresh before any consumer of bundle_data.c
384390
# builds. Wiring only ${NAME}-ui (DPF's static-lib aggregator) is not

src/PluginDSP.cpp

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "lsdj/SampleCache.hpp"
1818
#include "project/ProjectSerialization.hpp"
1919
#include "system/SystemTypes.hpp"
20+
#include "util/Base64.hpp"
2021
#include "system/sameboy/SameBoyConfig.hpp"
2122
#include "system/sameboy/SameBoySystem.hpp"
2223
#include "system/sameboy/roles/LsdjKitPatchRole.hpp"
@@ -129,8 +130,13 @@ class LVGLPluginDSP : public Plugin {
129130
{
130131
if (std::strcmp(key, "project") != 0) return String();
131132
try {
132-
const std::string json = projectConfigToJson(project.snapshotConfig());
133-
return String(json.c_str());
133+
const auto zip = projectConfigToZip(project.snapshotConfig());
134+
// DPF state is a NUL-terminated UTF-8 string, so the binary zip
135+
// blob is wrapped in base64. The inner zip is already deflated,
136+
// so the base64 layer's 33% overhead applies to compressed bytes
137+
// — still a large net win over today's JSON+inline-base64.
138+
const std::string encoded = base64::encode(zip);
139+
return String(encoded.c_str());
134140
} catch (const std::exception& e) {
135141
d_stderr("[PluginDSP] getState serialization failed: %s", e.what());
136142
return String();
@@ -140,42 +146,36 @@ class LVGLPluginDSP : public Plugin {
140146
void setState(const char* key, const char* value) override
141147
{
142148
if (std::strcmp(key, "project") != 0) return;
143-
applyProjectFromJson(value);
144-
}
145-
146-
// Replace the running project with one parsed from a JSON blob. Shared by
147-
// DPF setState (host-driven save/restore) and Command::LoadProject (the
148-
// user-driven "Load project" menu entry, fed in via the UI thread).
149-
// Caller is responsible for the threading context — setState runs DSP-side
150-
// before activate; Command::LoadProject runs DSP-side during the run-loop
151-
// command drain, same window AddSystem/etc. already mutate the project.
152-
void applyProjectFromJson(const char* json)
153-
{
154-
if (json == nullptr || json[0] == '\0') return;
155-
156-
std::optional<ProjectConfig> parsed;
157-
try {
158-
parsed = projectConfigFromJson(std::string_view(json));
159-
} catch (const std::exception& e) {
160-
d_stderr("[PluginDSP] applyProjectFromJson parse exception: %s", e.what());
161-
return;
162-
}
149+
if (value == nullptr || value[0] == '\0') return;
150+
const auto decoded = base64::decode(value);
151+
auto parsed = projectConfigFromZip(decoded);
163152
if (!parsed) {
164-
d_stderr("[PluginDSP] applyProjectFromJson: failed to parse project JSON");
153+
d_stderr("[PluginDSP] setState: failed to parse project zip");
165154
return;
166155
}
156+
applyProjectFromConfig(*parsed);
157+
}
167158

159+
// Replace the running project with a fully-parsed config. Shared by DPF
160+
// setState (host-driven save/restore) and Command::LoadProject (the
161+
// user-driven "Load project" menu entry, fed in via the UI thread which
162+
// does the file IO and zip parse). Caller is responsible for the
163+
// threading context — setState runs DSP-side before activate;
164+
// Command::LoadProject runs DSP-side during the run-loop command drain,
165+
// same window AddSystem/etc. already mutate the project.
166+
void applyProjectFromConfig(const ProjectConfig& parsed)
167+
{
168168
// Tear down current systems. Non-RT (deletes GB instances) — same
169169
// category of work AddSystem/RemoveSystem already do during command
170170
// drain.
171171
project.clearSystems();
172172
project.config() = ProjectConfig{};
173173

174174
SystemId firstAdded = 0;
175-
for (const auto& sysConfig : parsed->systems) {
175+
for (const auto& sysConfig : parsed.systems) {
176176
const SystemId id = project.addSystem(sysConfig);
177177
if (id == 0) {
178-
d_stderr("[PluginDSP] applyProjectFromJson: addSystem failed for one entry");
178+
d_stderr("[PluginDSP] applyProjectFromConfig: addSystem failed for one entry");
179179
continue;
180180
}
181181
if (firstAdded == 0) firstAdded = id;
@@ -184,7 +184,7 @@ class LVGLPluginDSP : public Plugin {
184184

185185
// Notify the UI (if attached) so it drops its cached project view.
186186
if (!events.tryPush(Event::makeConfigChanged()))
187-
d_stderr("[PluginDSP] applyProjectFromJson: event queue full; dropping ConfigChanged");
187+
d_stderr("[PluginDSP] applyProjectFromConfig: event queue full; dropping ConfigChanged");
188188
}
189189

190190
// ----------------------------------------------------------------------------------------------------------------
@@ -445,7 +445,7 @@ class LVGLPluginDSP : public Plugin {
445445
kitCfg->kits.push_back(std::move(fresh));
446446
slot = &kitCfg->kits.back();
447447
}
448-
slot->compiledBytes = Base64Bytes(*owned);
448+
slot->compiledBytes = *owned;
449449
slot->compiledHash =
450450
rp::lsdj::SampleCache::hashBytes(owned->data(), owned->size());
451451
break;
@@ -466,19 +466,19 @@ class LVGLPluginDSP : public Plugin {
466466
} break;
467467

468468
case Command::Kind::LoadProject: {
469-
std::string* json = cmd.payload.loadProject.json;
470-
if (json) {
471-
applyProjectFromJson(json->c_str());
472-
// applyProjectFromJson constructs SameBoySystems via
469+
ProjectConfig* config = cmd.payload.loadProject.config;
470+
if (config) {
471+
applyProjectFromConfig(*config);
472+
// applyProjectFromConfig constructs SameBoySystems via
473473
// Project::addSystem but doesn't activate them — the
474474
// setState path relies on DPF calling activate() after
475475
// setState, but Command::LoadProject runs mid-run()
476476
// with no following activate. Activate now so the new
477477
// systems actually start emulating; SameBoySystem's
478478
// onActivate is idempotent for already-active systems.
479479
project.onActivate(fSampleRate);
480-
delete json;
481-
// applyProjectFromJson already pushes ConfigChanged.
480+
delete config;
481+
// applyProjectFromConfig already pushes ConfigChanged.
482482
// Don't double-emit by setting projectMutated.
483483
}
484484
} break;

src/PluginRpcService.cpp

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <filesystem>
88
#include <fstream>
99
#include <memory>
10+
#include <span>
1011
#include <string_view>
1112
#include <utility>
1213
#include <vector>
@@ -68,6 +69,14 @@ bool spillString(const std::string& path, const std::string& data) {
6869
return out.good();
6970
}
7071

72+
bool spillBytes(const std::string& path, std::span<const std::uint8_t> data) {
73+
std::ofstream out(path, std::ios::binary | std::ios::trunc);
74+
if (!out) return false;
75+
out.write(reinterpret_cast<const char*>(data.data()),
76+
static_cast<std::streamsize>(data.size()));
77+
return out.good();
78+
}
79+
7180
} // namespace
7281

7382
PluginRpcService::PluginRpcService(Project* project,
@@ -149,7 +158,7 @@ SystemBase* PluginRpcService::buildSystemFromPath(const std::string& path) {
149158
sav.replace_extension(".sav");
150159
std::vector<std::uint8_t> sramBytes = slurp(sav.string());
151160
if (!sramBytes.empty())
152-
cfg.sram = Base64Bytes(std::move(sramBytes));
161+
cfg.sram = std::move(sramBytes);
153162
}
154163

155164
auto sys = std::make_unique<SameBoySystem>(id, cfg, std::move(bytes));
@@ -219,15 +228,20 @@ bool PluginRpcService::saveProjectToPath(const std::string& path) {
219228
emit("project-error", path);
220229
return false;
221230
}
222-
std::string json;
231+
std::vector<std::uint8_t> zip;
223232
try {
224-
json = projectConfigToJson(project_->snapshotConfig());
233+
zip = projectConfigToZip(project_->snapshotConfig());
225234
} catch (const std::exception& e) {
226235
std::fprintf(stderr, "saveProjectToPath: serialize failed: %s\n", e.what());
227236
emit("project-error", path);
228237
return false;
229238
}
230-
if (!spillString(path, json)) {
239+
if (zip.empty()) {
240+
std::fprintf(stderr, "saveProjectToPath: zip serialization produced empty buffer\n");
241+
emit("project-error", path);
242+
return false;
243+
}
244+
if (!spillBytes(path, zip)) {
231245
std::fprintf(stderr, "saveProjectToPath: write failed for '%s'\n", path.c_str());
232246
emit("project-error", path);
233247
return false;
@@ -243,14 +257,20 @@ bool PluginRpcService::loadProjectFromPath(const std::string& path) {
243257
emit("project-error", path);
244258
return false;
245259
}
246-
std::string json = slurpString(path);
247-
if (json.empty()) {
260+
const auto bytes = slurp(path);
261+
if (bytes.empty()) {
248262
std::fprintf(stderr, "loadProjectFromPath: empty / unreadable '%s'\n", path.c_str());
249263
emit("project-error", path);
250264
return false;
251265
}
252-
// Heap-allocate the JSON; DSP frees after parsing.
253-
auto* heap = new std::string(std::move(json));
266+
auto parsed = projectConfigFromZip(bytes);
267+
if (!parsed) {
268+
std::fprintf(stderr, "loadProjectFromPath: failed to parse zip '%s'\n", path.c_str());
269+
emit("project-error", path);
270+
return false;
271+
}
272+
// Heap-allocate the parsed config; DSP frees after applying.
273+
auto* heap = new ProjectConfig(std::move(*parsed));
254274
if (!commands_->tryPush(Command::makeLoadProject(heap))) {
255275
std::fprintf(stderr, "loadProjectFromPath: command queue full\n");
256276
delete heap;

src/project/Project.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ SystemId Project::addSystem(const SystemConfig& config) {
4444
// Prefer embedded ROM bytes (round-tripped through DPF state) over
4545
// re-reading from disk. Only fall back to slurpFile when bytes are
4646
// absent — covers legacy/dev paths and the embedRom=false opt-out.
47-
std::vector<std::uint8_t> rom = sb->romBytes.bytes();
47+
std::vector<std::uint8_t> rom = sb->romBytes;
4848
if (rom.empty())
4949
rom = slurpFile(sb->romPath);
5050
if (rom.empty()) {
@@ -60,7 +60,7 @@ SystemId Project::addSystem(const SystemConfig& config) {
6060
}
6161

6262
if (const auto* mb = rfl::get_if<MesenConfig>(&config.variant())) {
63-
std::vector<std::uint8_t> rom = mb->romBytes.bytes();
63+
std::vector<std::uint8_t> rom = mb->romBytes;
6464
if (rom.empty())
6565
rom = slurpFile(mb->romPath);
6666
if (rom.empty()) {
@@ -78,7 +78,7 @@ SystemId Project::addSystem(const SystemConfig& config) {
7878
}
7979

8080
if (const auto* gb = rfl::get_if<GbaSystemConfig>(&config.variant())) {
81-
std::vector<std::uint8_t> rom = gb->romBytes.bytes();
81+
std::vector<std::uint8_t> rom = gb->romBytes;
8282
if (rom.empty())
8383
rom = slurpFile(gb->romPath);
8484
if (rom.empty()) {

0 commit comments

Comments
 (0)