mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-17 23:39:18 +02:00
New Components/ Prepare to new scene loading system
This commit is contained in:
@@ -164,6 +164,7 @@ set(SOURCES
|
||||
"audio/audiorenderer.cpp"
|
||||
"utilities/motiontelemetry.cpp"
|
||||
"utilities/utilities.cpp"
|
||||
"utilities/AsyncFilePreloader.cpp"
|
||||
"application/uitranscripts.cpp"
|
||||
"world/station.cpp"
|
||||
"application/application.cpp"
|
||||
@@ -431,7 +432,9 @@ if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC)
|
||||
entitysystem/systems/VehicleSyncSystem.cpp
|
||||
entitysystem/systems/VehicleSyncSystem.h
|
||||
entitysystem/systems/BillboardSystem.cpp
|
||||
entitysystem/systems/BillboardSystem.h)
|
||||
entitysystem/systems/BillboardSystem.h
|
||||
entitysystem/ECSPersistence.cpp
|
||||
entitysystem/ECSPersistence.h)
|
||||
add_compile_options("/utf-8")
|
||||
endif()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "entitysystem/ECScene.h"
|
||||
#include "entitysystem/components/BasicComponents.h"
|
||||
#include "entitysystem/components/RenderComponents.h"
|
||||
#include "entitysystem/ECSPersistence.h"
|
||||
|
||||
driver_ui::driver_ui()
|
||||
{
|
||||
@@ -366,6 +367,100 @@ void driver_ui::register_driver_commands()
|
||||
dev::Console.print_ok("ParticleEmitter added to: " + args[1]);
|
||||
});
|
||||
|
||||
con.register_command("ecs.addspotlight",
|
||||
"Spawn SpotLight at position: ecs.addspotlight <name> <x> <y> <z> [r g b] [intensity] [range]",
|
||||
[](const dev::args_t &args) {
|
||||
if (args.size() < 5) { dev::Console.print_error("Usage: ecs.addspotlight <name> <x> <y> <z> [r g b] [intensity] [range]"); return; }
|
||||
ECScene *scene = Application.sceneManager.CurrentScene();
|
||||
if (!scene) { dev::Console.print_warn("No active scene"); return; }
|
||||
ECWorld &world = scene->World();
|
||||
|
||||
auto entity = world.CreateEntity();
|
||||
|
||||
auto& id = world.AddComponent<ECSComponent::Identification>(entity);
|
||||
id.Name = args[1];
|
||||
|
||||
auto& t = world.AddComponent<ECSComponent::Transform>(entity);
|
||||
t.Position = glm::dvec3(std::stod(args[2]), std::stod(args[3]), std::stod(args[4]));
|
||||
|
||||
auto& spot = world.AddComponent<ECSComponent::SpotLight>(entity);
|
||||
if (args.size() > 7) {
|
||||
spot.Color = glm::vec3(std::stof(args[5]), std::stof(args[6]), std::stof(args[7]));
|
||||
}
|
||||
if (args.size() > 8) spot.Intensity = std::stof(args[8]);
|
||||
if (args.size() > 9) spot.Range = std::stof(args[9]);
|
||||
spot.Enabled = true;
|
||||
|
||||
dev::Console.print_ok("SpotLight '" + args[1] + "' spawned at ("
|
||||
+ args[2] + ", " + args[3] + ", " + args[4] + ")");
|
||||
});
|
||||
|
||||
con.register_command("ecs.clone",
|
||||
"Clone entity with optional offset: ecs.clone <src> <newname> [dx dy dz]",
|
||||
[](const dev::args_t &args) {
|
||||
if (args.size() < 3) { dev::Console.print_error("Usage: ecs.clone <src> <newname> [dx dy dz]"); return; }
|
||||
ECScene *scene = Application.sceneManager.CurrentScene();
|
||||
if (!scene) { dev::Console.print_warn("No active scene"); return; }
|
||||
ECWorld &world = scene->World();
|
||||
|
||||
entt::entity src = world.FindEntityByName(args[1]);
|
||||
if (src == entt::null) { dev::Console.print_error("Entity not found: " + args[1]); return; }
|
||||
if (world.FindEntityByName(args[2]) != entt::null) { dev::Console.print_error("Name already exists: " + args[2]); return; }
|
||||
|
||||
glm::dvec3 offset{0.0};
|
||||
if (args.size() >= 6) {
|
||||
offset = glm::dvec3(std::stod(args[3]), std::stod(args[4]), std::stod(args[5]));
|
||||
}
|
||||
|
||||
auto dst = world.CreateEntity();
|
||||
auto &id = world.AddComponent<ECSComponent::Identification>(dst);
|
||||
id.Name = args[2];
|
||||
|
||||
if (auto *t = world.GetComponent<ECSComponent::Transform>(src)) {
|
||||
auto &nt = world.AddComponent<ECSComponent::Transform>(dst);
|
||||
nt = *t;
|
||||
nt.Position += offset;
|
||||
}
|
||||
if (auto *s = world.GetComponent<ECSComponent::SpotLight>(src))
|
||||
world.AddComponent<ECSComponent::SpotLight>(dst) = *s;
|
||||
if (auto *p = world.GetComponent<ECSComponent::ParticleEmitter>(src))
|
||||
world.AddComponent<ECSComponent::ParticleEmitter>(dst) = *p;
|
||||
if (auto *b = world.GetComponent<ECSComponent::Billboard>(src))
|
||||
world.AddComponent<ECSComponent::Billboard>(dst) = *b;
|
||||
if (auto *l = world.GetComponent<ECSComponent::Line>(src))
|
||||
world.AddComponent<ECSComponent::Line>(dst) = *l;
|
||||
if (auto *lod = world.GetComponent<ECSComponent::LODController>(src))
|
||||
world.AddComponent<ECSComponent::LODController>(dst) = *lod;
|
||||
|
||||
dev::Console.print_ok("Cloned '" + args[1] + "' → '" + args[2] + "'");
|
||||
});
|
||||
|
||||
con.register_command("ecs.save",
|
||||
"Save ECS entities to JSON: ecs.save [filename=ecs_save.json]",
|
||||
[](const dev::args_t &args) {
|
||||
ECScene *scene = Application.sceneManager.CurrentScene();
|
||||
if (!scene) { dev::Console.print_warn("No active scene"); return; }
|
||||
std::string filename = (args.size() > 1) ? args[1] : "ecs_save.json";
|
||||
int n = ecs_persistence::save(scene->World(), filename);
|
||||
if (n >= 0)
|
||||
dev::Console.print_ok("Saved " + std::to_string(n) + " entities to " + filename);
|
||||
else
|
||||
dev::Console.print_error("Cannot write: " + filename);
|
||||
});
|
||||
|
||||
con.register_command("ecs.load",
|
||||
"Load ECS entities from JSON: ecs.load [filename=ecs_save.json]",
|
||||
[](const dev::args_t &args) {
|
||||
ECScene *scene = Application.sceneManager.CurrentScene();
|
||||
if (!scene) { dev::Console.print_warn("No active scene"); return; }
|
||||
std::string filename = (args.size() > 1) ? args[1] : "ecs_save.json";
|
||||
int n = ecs_persistence::load(scene->World(), filename);
|
||||
if (n >= 0)
|
||||
dev::Console.print_ok("Loaded " + std::to_string(n) + " entities from " + filename);
|
||||
else
|
||||
dev::Console.print_error("Cannot read or parse: " + filename);
|
||||
});
|
||||
|
||||
con.print_info("Driver console ready. Type 'help' for commands.");
|
||||
}
|
||||
|
||||
|
||||
@@ -579,18 +579,22 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
|
||||
float pos[3] = { static_cast<float>(transform->Position.x), static_cast<float>(transform->Position.y), static_cast<float>(transform->Position.z) };
|
||||
float pos[3] = {
|
||||
static_cast<float>(transform->Position.x),
|
||||
static_cast<float>(transform->Position.y),
|
||||
static_cast<float>(transform->Position.z) };
|
||||
if (ImGui::DragFloat3("Position", pos, 0.1f)) {
|
||||
transform->Position.x = pos[0];
|
||||
transform->Position.y = pos[1];
|
||||
transform->Position.z = pos[2];
|
||||
}
|
||||
|
||||
if(ImGui::DragFloat3("Position", pos, 0.1f)){
|
||||
glm::vec3 euler = glm::degrees(glm::eulerAngles(transform->Rotation));
|
||||
if (ImGui::DragFloat3("Rotation (deg)", &euler.x, 0.5f)) {
|
||||
transform->Rotation = glm::quat(glm::radians(euler));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ImGui::DragFloat3("Rotation",
|
||||
&transform->Rotation.x, 0.1, -360.0, 360.0);
|
||||
|
||||
ImGui::DragFloat3("Scale",
|
||||
&transform->Scale.x, 0.01, 0.0, 100000.0);
|
||||
ImGui::DragFloat3("Scale", &transform->Scale.x, 0.01f, 0.0f, 100000.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,13 +603,9 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Velocity", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::DragFloat3("Value",
|
||||
&velocity->Value.x, 0.1);
|
||||
|
||||
if (ImGui::Button("Stop"))
|
||||
{
|
||||
velocity->Value = glm::dvec3(0.0);
|
||||
}
|
||||
ImGui::DragFloat3("Value", &velocity->Value.x, 0.1f);
|
||||
if (ImGui::Button("Stop"))
|
||||
velocity->Value = glm::vec3(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,10 +614,12 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
|
||||
{
|
||||
if (ImGui::CollapsingHeader("LODController", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
float range_min = lod->RangeMin;
|
||||
float range_max = lod->RangeMax;
|
||||
ImGui::DragFloat("Range Min", &range_min, 0.1, 0.0, 100000.0);
|
||||
ImGui::DragFloat("Range Max", &range_max, 0.1, 0.0, 100000.0);
|
||||
float range_min = static_cast<float>(lod->RangeMin);
|
||||
float range_max = static_cast<float>(lod->RangeMax);
|
||||
if (ImGui::DragFloat("Range Min", &range_min, 0.1f, 0.0f, 100000.0f))
|
||||
lod->RangeMin = range_min;
|
||||
if (ImGui::DragFloat("Range Max", &range_max, 0.1f, 0.0f, 100000.0f))
|
||||
lod->RangeMax = range_max;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,7 +648,6 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
|
||||
}
|
||||
|
||||
// SpotLight
|
||||
|
||||
if (auto* light = world.GetComponent<ECSComponent::SpotLight>(m_selected_entity))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("SpotLight", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
@@ -661,6 +662,58 @@ void ui_layer::render_entity_hierarchy(ECWorld& world)
|
||||
}
|
||||
}
|
||||
|
||||
// SunLight
|
||||
if (auto* sun = world.GetComponent<ECSComponent::SunLight>(m_selected_entity))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("SunLight", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::Checkbox("Enabled", &sun->Enabled);
|
||||
ImGui::ColorEdit3("Color", &sun->Color.x);
|
||||
ImGui::DragFloat("Intensity", &sun->Intensity, 0.01f, 0.0f, 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// ParticleEmitter
|
||||
if (auto* em = world.GetComponent<ECSComponent::ParticleEmitter>(m_selected_entity))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("ParticleEmitter", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::Checkbox("Active", &em->isActive);
|
||||
ImGui::DragFloat("Spawn Rate", &em->spawnRate, 0.5f, 0.0f, 1000.0f);
|
||||
ImGui::DragFloat("Lifetime", &em->particleLifetime, 0.1f, 0.1f, 60.0f);
|
||||
ImGui::DragFloat("Air Resistance", &em->airResistance, 0.01f, 0.0f, 5.0f);
|
||||
ImGui::DragFloat3("Gravity", &em->gravity.x, 0.01f);
|
||||
ImGui::DragFloat4("Color Fade", &em->colorFade.x, 0.01f, -1.0f, 1.0f);
|
||||
ImGui::Text("Particles alive: %zu", em->particles.size());
|
||||
if (ImGui::Button("Clear particles"))
|
||||
em->particles.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Billboard
|
||||
if (auto* bill = world.GetComponent<ECSComponent::Billboard>(m_selected_entity))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Billboard", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::Checkbox("Active", &bill->active);
|
||||
ImGui::DragFloat("Size", &bill->size, 0.05f, 0.01f, 100.0f);
|
||||
ImGui::Text("Texture: %s", bill->texturePath.ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Line
|
||||
if (auto* line = world.GetComponent<ECSComponent::Line>(m_selected_entity))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Line", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::Checkbox("Active", &line->active);
|
||||
ImGui::DragFloat3("Start", &line->start.x, 0.1f);
|
||||
ImGui::DragFloat3("End", &line->end.x, 0.1f);
|
||||
ImGui::ColorEdit3("Color", &line->color.x);
|
||||
ImGui::DragFloat("Thickness", &line->thickness, 0.1f, 0.1f, 20.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
172
entitysystem/ECSPersistence.cpp
Normal file
172
entitysystem/ECSPersistence.cpp
Normal file
@@ -0,0 +1,172 @@
|
||||
#include "stdafx.h"
|
||||
#include "ECSPersistence.h"
|
||||
|
||||
#include "entitysystem/ECWorld.h"
|
||||
#include "entitysystem/components/BasicComponents.h"
|
||||
#include "entitysystem/components/RenderComponents.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include <fstream>
|
||||
|
||||
namespace ecs_persistence
|
||||
{
|
||||
|
||||
int save(ECWorld &world, const std::string &path)
|
||||
{
|
||||
nlohmann::json root = nlohmann::json::array();
|
||||
|
||||
for (auto entity : world.GetEntities()) {
|
||||
auto *idComp = world.GetComponent<ECSComponent::Identification>(entity);
|
||||
if (!idComp) continue;
|
||||
|
||||
nlohmann::json ent;
|
||||
ent["name"] = idComp->Name.ToString();
|
||||
|
||||
if (auto *t = world.GetComponent<ECSComponent::Transform>(entity)) {
|
||||
ent["Transform"] = {
|
||||
{"px", t->Position.x}, {"py", t->Position.y}, {"pz", t->Position.z},
|
||||
{"rx", t->Rotation.x}, {"ry", t->Rotation.y}, {"rz", t->Rotation.z}, {"rw", t->Rotation.w},
|
||||
{"sx", t->Scale.x}, {"sy", t->Scale.y}, {"sz", t->Scale.z}
|
||||
};
|
||||
}
|
||||
if (auto *s = world.GetComponent<ECSComponent::SpotLight>(entity)) {
|
||||
ent["SpotLight"] = {
|
||||
{"r", s->Color.x}, {"g", s->Color.y}, {"b", s->Color.z},
|
||||
{"intensity", s->Intensity}, {"range", s->Range},
|
||||
{"innerAngle", s->InnerAngle}, {"outerAngle", s->OuterAngle},
|
||||
{"enabled", s->Enabled}, {"castShadows", s->CastShadows}
|
||||
};
|
||||
}
|
||||
if (auto *p = world.GetComponent<ECSComponent::ParticleEmitter>(entity)) {
|
||||
ent["ParticleEmitter"] = {
|
||||
{"active", p->isActive}, {"spawnRate", p->spawnRate},
|
||||
{"lifetime", p->particleLifetime}, {"airResistance", p->airResistance},
|
||||
{"gx", p->gravity.x}, {"gy", p->gravity.y}, {"gz", p->gravity.z},
|
||||
{"fadex", p->colorFade.x}, {"fadey", p->colorFade.y},
|
||||
{"fadez", p->colorFade.z}, {"fadew", p->colorFade.w}
|
||||
};
|
||||
}
|
||||
if (auto *b = world.GetComponent<ECSComponent::Billboard>(entity)) {
|
||||
ent["Billboard"] = {
|
||||
{"texture", b->texturePath.ToString()},
|
||||
{"size", b->size}, {"active", b->active}
|
||||
};
|
||||
}
|
||||
if (auto *l = world.GetComponent<ECSComponent::Line>(entity)) {
|
||||
ent["Line"] = {
|
||||
{"sx", l->start.x}, {"sy", l->start.y}, {"sz", l->start.z},
|
||||
{"ex", l->end.x}, {"ey", l->end.y}, {"ez", l->end.z},
|
||||
{"r", l->color.x}, {"g", l->color.y}, {"b", l->color.z},
|
||||
{"thickness", l->thickness}, {"active", l->active}
|
||||
};
|
||||
}
|
||||
if (auto *lod = world.GetComponent<ECSComponent::LODController>(entity)) {
|
||||
ent["LODController"] = {{"min", lod->RangeMin}, {"max", lod->RangeMax}};
|
||||
}
|
||||
if (world.HasComponent<ECSComponent::Disabled>(entity))
|
||||
ent["disabled"] = true;
|
||||
|
||||
// skip entities that are purely runtime-managed (VehicleRef, MeshRenderer linked to legacy)
|
||||
bool runtimeOnly =
|
||||
world.HasComponent<ECSComponent::VehicleRef>(entity) ||
|
||||
(world.HasComponent<ECSComponent::MeshRenderer>(entity) &&
|
||||
world.GetComponent<ECSComponent::MeshRenderer>(entity)->modelInstance != nullptr);
|
||||
if (runtimeOnly) continue;
|
||||
|
||||
root.push_back(ent);
|
||||
}
|
||||
|
||||
std::ofstream f(path);
|
||||
if (!f) {
|
||||
WriteLog("[ECS] ECSPersistence: cannot write to " + path);
|
||||
return -1;
|
||||
}
|
||||
f << root.dump(2);
|
||||
WriteLog("[ECS] ECSPersistence: saved " + std::to_string(root.size()) + " entities to " + path);
|
||||
return static_cast<int>(root.size());
|
||||
}
|
||||
|
||||
int load(ECWorld &world, const std::string &path)
|
||||
{
|
||||
std::ifstream f(path);
|
||||
if (!f) return -1; // file doesn't exist — silently skip
|
||||
|
||||
nlohmann::json root;
|
||||
try { root = nlohmann::json::parse(f); }
|
||||
catch (const std::exception &e) {
|
||||
WriteLog("[ECS] ECSPersistence: JSON parse error in " + path + ": " + e.what());
|
||||
return -1;
|
||||
}
|
||||
|
||||
int created = 0;
|
||||
for (auto const &ent : root) {
|
||||
if (!ent.contains("name")) continue;
|
||||
std::string name = ent["name"].get<std::string>();
|
||||
if (world.FindEntityByName(name) != entt::null) continue;
|
||||
|
||||
auto entity = world.CreateEntity();
|
||||
auto &id = world.AddComponent<ECSComponent::Identification>(entity);
|
||||
id.Name = name;
|
||||
|
||||
if (ent.contains("Transform")) {
|
||||
auto &j = ent["Transform"];
|
||||
auto &t = world.AddComponent<ECSComponent::Transform>(entity);
|
||||
t.Position = glm::dvec3(double(j["px"]), double(j["py"]), double(j["pz"]));
|
||||
t.Rotation = glm::quat(float(j["rw"]), float(j["rx"]), float(j["ry"]), float(j["rz"]));
|
||||
t.Scale = glm::vec3(float(j["sx"]), float(j["sy"]), float(j["sz"]));
|
||||
}
|
||||
if (ent.contains("SpotLight")) {
|
||||
auto &j = ent["SpotLight"];
|
||||
auto &s = world.AddComponent<ECSComponent::SpotLight>(entity);
|
||||
s.Color = glm::vec3(float(j["r"]), float(j["g"]), float(j["b"]));
|
||||
s.Intensity = j["intensity"];
|
||||
s.Range = j["range"];
|
||||
s.InnerAngle = j["innerAngle"];
|
||||
s.OuterAngle = j["outerAngle"];
|
||||
s.Enabled = j["enabled"];
|
||||
s.CastShadows = j["castShadows"];
|
||||
}
|
||||
if (ent.contains("ParticleEmitter")) {
|
||||
auto &j = ent["ParticleEmitter"];
|
||||
auto &p = world.AddComponent<ECSComponent::ParticleEmitter>(entity);
|
||||
p.isActive = j["active"];
|
||||
p.spawnRate = j["spawnRate"];
|
||||
p.particleLifetime = j["lifetime"];
|
||||
p.airResistance = j["airResistance"];
|
||||
p.gravity = glm::vec3(float(j["gx"]), float(j["gy"]), float(j["gz"]));
|
||||
p.colorFade = glm::vec4(float(j["fadex"]), float(j["fadey"]),
|
||||
float(j["fadez"]), float(j["fadew"]));
|
||||
}
|
||||
if (ent.contains("Billboard")) {
|
||||
auto &j = ent["Billboard"];
|
||||
auto &b = world.AddComponent<ECSComponent::Billboard>(entity);
|
||||
b.texturePath = FName(j["texture"].get<std::string>());
|
||||
b.size = j["size"];
|
||||
b.active = j["active"];
|
||||
}
|
||||
if (ent.contains("Line")) {
|
||||
auto &j = ent["Line"];
|
||||
auto &l = world.AddComponent<ECSComponent::Line>(entity);
|
||||
l.start = glm::vec3(float(j["sx"]), float(j["sy"]), float(j["sz"]));
|
||||
l.end = glm::vec3(float(j["ex"]), float(j["ey"]), float(j["ez"]));
|
||||
l.color = glm::vec3(float(j["r"]), float(j["g"]), float(j["b"]));
|
||||
l.thickness = j["thickness"];
|
||||
l.active = j["active"];
|
||||
}
|
||||
if (ent.contains("LODController")) {
|
||||
auto &j = ent["LODController"];
|
||||
auto &lod = world.AddComponent<ECSComponent::LODController>(entity);
|
||||
lod.RangeMin = j["min"];
|
||||
lod.RangeMax = j["max"];
|
||||
}
|
||||
if (ent.value("disabled", false))
|
||||
world.AddComponent<ECSComponent::Disabled>(entity);
|
||||
|
||||
++created;
|
||||
}
|
||||
|
||||
WriteLog("[ECS] ECSPersistence: loaded " + std::to_string(created) + " entities from " + path);
|
||||
return created;
|
||||
}
|
||||
|
||||
} // namespace ecs_persistence
|
||||
12
entitysystem/ECSPersistence.h
Normal file
12
entitysystem/ECSPersistence.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class ECWorld;
|
||||
|
||||
namespace ecs_persistence
|
||||
{
|
||||
// Save all named entities to a JSON file. Returns number of entities saved, or -1 on error.
|
||||
int save(ECWorld &world, const std::string &path);
|
||||
// Load entities from a JSON file. Skips entities whose name already exists. Returns count loaded, or -1 on error.
|
||||
int load(ECWorld &world, const std::string &path);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "ECScene.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "simulation/simulation.h"
|
||||
|
||||
ECScene::ECScene()
|
||||
: m_loaded(false)
|
||||
@@ -39,6 +40,9 @@ void ECScene::Update(float dt)
|
||||
if (!m_loaded)
|
||||
return;
|
||||
|
||||
if (!simulation::is_ready)
|
||||
return;
|
||||
|
||||
m_systems.Update(m_world, dt);
|
||||
OnUpdate(dt);
|
||||
}
|
||||
|
||||
@@ -12,15 +12,24 @@ entt::entity ECWorld::CreateEntity()
|
||||
|
||||
void ECWorld::DestroyEntity(entt::entity entity)
|
||||
{
|
||||
if (m_registry.valid(entity))
|
||||
if (m_registry.valid(entity)) {
|
||||
if (auto *id = m_registry.try_get<ECSComponent::Identification>(entity))
|
||||
m_nameIndex.erase(id->Name.ToString());
|
||||
m_registry.destroy(entity);
|
||||
}
|
||||
}
|
||||
|
||||
void ECWorld::Clear()
|
||||
{
|
||||
m_nameIndex.clear();
|
||||
m_registry.clear();
|
||||
}
|
||||
|
||||
void ECWorld::UpdateNameIndex(entt::entity entity, const std::string &name)
|
||||
{
|
||||
m_nameIndex[name] = entity;
|
||||
}
|
||||
|
||||
bool ECWorld::IsAlive(entt::entity entity) const
|
||||
{
|
||||
return m_registry.valid(entity);
|
||||
@@ -29,18 +38,8 @@ bool ECWorld::IsAlive(entt::entity entity) const
|
||||
|
||||
entt::entity ECWorld::FindEntityByName(const std::string& name) const
|
||||
{
|
||||
auto view = m_registry.view<ECSComponent::Identification>();
|
||||
|
||||
for (auto entity : view)
|
||||
{
|
||||
const auto& id = view.get<ECSComponent::Identification>(entity);
|
||||
if (id.Name.ToString() == name)
|
||||
{
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return entt::null;
|
||||
auto it = m_nameIndex.find(name);
|
||||
return (it != m_nameIndex.end()) ? it->second : entt::null;
|
||||
}
|
||||
|
||||
entt::registry& ECWorld::Registry()
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
#include <entt/entt.hpp>
|
||||
#include <utility>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
// Forward declaration so the name-index branch in AddComponent<T> compiles in
|
||||
// translation units that include ECWorld.h without BasicComponents.h.
|
||||
// if constexpr discards the branch for non-Identification T, but the name must
|
||||
// still resolve at parse time.
|
||||
namespace ECSComponent { struct Identification; }
|
||||
|
||||
class ECWorld
|
||||
{
|
||||
@@ -22,6 +30,8 @@ public:
|
||||
entt::entity CreateEntity();
|
||||
void DestroyEntity(entt::entity entity);
|
||||
void Clear();
|
||||
// Updates the name→entity index after an Identification component's Name is changed at runtime.
|
||||
void UpdateNameIndex(entt::entity entity, const std::string &name);
|
||||
|
||||
bool IsAlive(entt::entity entity) const;
|
||||
|
||||
@@ -105,6 +115,7 @@ public:
|
||||
|
||||
private:
|
||||
entt::registry m_registry;
|
||||
std::unordered_map<std::string, entt::entity> m_nameIndex;
|
||||
};
|
||||
|
||||
template<typename T, typename... Args>
|
||||
@@ -122,7 +133,10 @@ T& ECWorld::AddComponent(entt::entity entity, Args&&... args)
|
||||
static T dummy{};
|
||||
return dummy;
|
||||
} else {
|
||||
return m_registry.get<T>(entity);
|
||||
auto &comp = m_registry.get<T>(entity);
|
||||
if constexpr (std::is_same_v<T, ECSComponent::Identification>)
|
||||
m_nameIndex[comp.Name.ToString()] = entity;
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "entitysystem/systems/LineSystem.h"
|
||||
#include "entitysystem/systems/VehicleSyncSystem.h"
|
||||
#include "entitysystem/systems/BillboardSystem.h"
|
||||
#include "entitysystem/ECSPersistence.h"
|
||||
#include "utilities/Logs.h"
|
||||
|
||||
GameScene::GameScene() = default;
|
||||
@@ -37,6 +38,8 @@ void GameScene::OnLoad()
|
||||
#ifdef _DEBUG
|
||||
CreateTestEntities();
|
||||
#endif
|
||||
|
||||
ecs_persistence::load(World(), "ecs_save.json");
|
||||
}
|
||||
|
||||
void GameScene::OnUpdate(float dt)
|
||||
@@ -45,6 +48,7 @@ void GameScene::OnUpdate(float dt)
|
||||
|
||||
void GameScene::OnUnload()
|
||||
{
|
||||
ecs_persistence::save(World(), "ecs_save.json");
|
||||
World().Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ void LightSystem::Update(ECWorld& world, float dt)
|
||||
|
||||
light_array::free_light_record rec;
|
||||
rec.position = transform.Position;
|
||||
rec.direction = glm::vec3(0.f, -1.f, 0.f); // default downward; rotation support can be added later
|
||||
rec.direction = glm::normalize(glm::mat3_cast(transform.Rotation) * glm::vec3(0.f, -1.f, 0.f));
|
||||
rec.color = spot.Color;
|
||||
rec.intensity = spot.Intensity;
|
||||
rec.range = spot.Range;
|
||||
|
||||
@@ -18,6 +18,7 @@ Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/utilities.h"
|
||||
#include "utilities/AsyncFilePreloader.h"
|
||||
#include "rendering/renderer.h"
|
||||
#include "utilities/Timer.h"
|
||||
#include "simulation/simulation.h"
|
||||
@@ -2357,10 +2358,34 @@ bool TSubModel::HasAnyVertexUserData() const
|
||||
return false;
|
||||
};
|
||||
|
||||
// Helper: a read-only streambuf backed by an existing memory buffer.
|
||||
struct membuf : std::streambuf {
|
||||
membuf(const char* begin, const char* end) {
|
||||
setg(const_cast<char*>(begin), const_cast<char*>(begin), const_cast<char*>(end));
|
||||
}
|
||||
};
|
||||
|
||||
void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic)
|
||||
{ // wczytanie modelu z pliku binarnego
|
||||
WriteLog("Loading binary format 3d model data from \"" + FileName + "\"...", logtype::model);
|
||||
|
||||
auto preloaded = GModelPreloader.get(FileName);
|
||||
if (preloaded && !preloaded->empty()) {
|
||||
// Fast path: file already in RAM — parse directly from memory, no disk I/O.
|
||||
membuf mb(preloaded->data(), preloaded->data() + preloaded->size());
|
||||
std::istream stream(&mb);
|
||||
uint32_t type = sn_utils::ld_uint32(stream);
|
||||
uint32_t size = sn_utils::ld_uint32(stream) - 8;
|
||||
if (type == MAKE_ID4('E', '3', 'D', '0')) {
|
||||
deserialize(stream, size, dynamic);
|
||||
WriteLog("Finished loading 3d model data from \"" + FileName + "\"", logtype::model);
|
||||
} else {
|
||||
ErrorLog("Bad model: unknown main chunk in file \"" + FileName + "\"", logtype::model);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Slow path: synchronous disk read.
|
||||
std::ifstream file(FileName, std::ios::binary);
|
||||
|
||||
uint32_t type = sn_utils::ld_uint32(file);
|
||||
@@ -2375,7 +2400,6 @@ void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic)
|
||||
}
|
||||
else
|
||||
{
|
||||
// throw std::runtime_error("e3d: unknown main chunk");
|
||||
ErrorLog("Bad model: unknown main chunk in file \"" + FileName + "\"", logtype::model);
|
||||
file.close();
|
||||
}
|
||||
@@ -2414,7 +2438,13 @@ void TModel3d::LoadFromTextFile(std::string const &FileName, bool dynamic)
|
||||
{ // wczytanie submodelu z pliku tekstowego
|
||||
WriteLog("Loading text format 3d model data from \"" + FileName + "\"...", logtype::model);
|
||||
iFlags |= 0x0200; // wczytano z pliku tekstowego (właścicielami tablic są submodle)
|
||||
cParser parser(FileName, cParser::buffer_FILE); // Ra: tu powinno być "models/"...
|
||||
|
||||
auto preloaded = GModelPreloader.get(FileName);
|
||||
cParser parser(
|
||||
preloaded && !preloaded->empty()
|
||||
? std::string(preloaded->data(), preloaded->size())
|
||||
: FileName,
|
||||
preloaded && !preloaded->empty() ? cParser::buffer_TEXT : cParser::buffer_FILE); // Ra: tu powinno być "models/"...
|
||||
TSubModel *SubModel;
|
||||
std::string token = parser.getToken<std::string>();
|
||||
while (token != "" || parser.eof())
|
||||
|
||||
@@ -30,9 +30,69 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "entitysystem/components/RenderComponents.h"
|
||||
#include "rendering/renderer.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/AsyncFilePreloader.h"
|
||||
|
||||
namespace simulation {
|
||||
|
||||
// Resolve a raw scenario token to the exact model file path that
|
||||
// TModel3d::LoadFromFile will eventually open, or "" if it isn't a model.
|
||||
// Mirrors TModelsManager::find_on_disk + TModel3d::LoadFromFile path logic so
|
||||
// the preloader caches under the same key the loader later looks up.
|
||||
static std::string resolve_model_path( std::string name ) {
|
||||
erase_extension( name );
|
||||
name = ToLower( name );
|
||||
|
||||
std::string base;
|
||||
for( auto const &ext : { std::string(".e3d"), std::string(".t3d") } ) {
|
||||
if( FileExists( name + ext ) ) { base = name; break; }
|
||||
if( FileExists( paths::models + name + ext ) ) { base = paths::models + name; break; }
|
||||
}
|
||||
if( base.empty() )
|
||||
return {};
|
||||
|
||||
if( Global.priorityLoadText3D && FileExists( base + ".t3d" ) )
|
||||
return base + ".t3d";
|
||||
if( FileExists( base + ".e3d" ) )
|
||||
return base + ".e3d";
|
||||
if( FileExists( base + ".t3d" ) )
|
||||
return base + ".t3d";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Runs on a background thread: tokenizes the scenario (includes expanded by
|
||||
// cParser) ahead of the main parse and queues any token that resolves to a
|
||||
// model file. Best-effort — false positives fail to load harmlessly and
|
||||
// missed models simply fall back to synchronous disk reads.
|
||||
static void prefetch_scan_worker( std::string scenariofile, std::string scenerypath,
|
||||
bool loadtraction, std::atomic<bool> *cancel ) {
|
||||
try {
|
||||
cParser scan( scenariofile, cParser::buffer_FILE, scenerypath, loadtraction );
|
||||
std::string tok;
|
||||
while( !( tok = scan.getToken<std::string>( false ) ).empty() ) {
|
||||
if( cancel->load( std::memory_order_relaxed ) )
|
||||
return;
|
||||
// cheap filter: model references carry a path separator or extension;
|
||||
// skips the millions of numeric vertex/coordinate tokens instantly.
|
||||
if( tok.size() < 4 )
|
||||
continue;
|
||||
bool pathlike =
|
||||
tok.find( '/' ) != std::string::npos
|
||||
|| tok.find( '\\' ) != std::string::npos
|
||||
|| ends_with( ToLower( tok ), ".t3d" )
|
||||
|| ends_with( ToLower( tok ), ".e3d" );
|
||||
if( !pathlike )
|
||||
continue;
|
||||
|
||||
std::string resolved = resolve_model_path( tok );
|
||||
if( !resolved.empty() )
|
||||
GModelPreloader.queue( resolved );
|
||||
}
|
||||
}
|
||||
catch( ... ) {
|
||||
// best-effort prefetch — never let a scan failure affect loading
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<deserializer_state>
|
||||
state_serializer::deserialize_begin( std::string const &Scenariofile ) {
|
||||
|
||||
@@ -112,6 +172,14 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) {
|
||||
state->input.injectString(Global.prepend_scn);
|
||||
}
|
||||
|
||||
// kick off background disk preloading: a scan thread races ahead of the main
|
||||
// parse discovering model files, worker threads read them into RAM, and
|
||||
// LoadFromBinFile/LoadFromTextFile parse straight from memory on the main thread.
|
||||
GModelPreloader.start();
|
||||
state->prefetch_thread = std::thread(
|
||||
prefetch_scan_worker, Scenariofile, Global.asCurrentSceneryPath,
|
||||
Global.bLoadTraction, &state->prefetch_cancel );
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -161,6 +229,13 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
Region->serialize( state->scenariofile );
|
||||
}
|
||||
|
||||
// loading finished — stop preloader threads and free the cached file buffers
|
||||
state->prefetch_cancel = true;
|
||||
if( state->prefetch_thread.joinable() )
|
||||
state->prefetch_thread.join();
|
||||
GModelPreloader.stop();
|
||||
GModelPreloader.clear();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -435,24 +510,38 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
||||
return;
|
||||
ECWorld& world = currentScene->World();
|
||||
|
||||
entt::entity entity = world.CreateEntity();
|
||||
// Only create ECS entities for node types that actually use them.
|
||||
// Geometry (triangles, lines) and infrastructure (track, traction) go through
|
||||
// the legacy pipeline exclusively — creating entities for them wastes time and memory.
|
||||
static const std::unordered_set<std::string> ecs_node_types = {
|
||||
"dynamic", "model", "sound"
|
||||
};
|
||||
const bool needs_entity = ecs_node_types.count(nodedata.type) > 0;
|
||||
|
||||
auto& transformComponent = world.AddComponent<ECSComponent::Transform>(entity);
|
||||
transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top();
|
||||
transformComponent.Rotation = Scratchpad.location.rotation;
|
||||
entt::entity entity = entt::null;
|
||||
if (needs_entity) {
|
||||
entity = world.CreateEntity();
|
||||
|
||||
if (nodedata.range_max > 0.0 || nodedata.range_min >= 0.0) {
|
||||
auto& lodComponent = world.AddComponent<ECSComponent::LODController>(entity);
|
||||
lodComponent.RangeMin = nodedata.range_min;
|
||||
lodComponent.RangeMax = nodedata.range_max;
|
||||
auto& transformComponent = world.AddComponent<ECSComponent::Transform>(entity);
|
||||
transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top();
|
||||
transformComponent.Rotation = Scratchpad.location.rotation;
|
||||
|
||||
if (nodedata.range_max > 0.0 || nodedata.range_min >= 0.0) {
|
||||
auto& lodComponent = world.AddComponent<ECSComponent::LODController>(entity);
|
||||
lodComponent.RangeMin = nodedata.range_min;
|
||||
lodComponent.RangeMax = nodedata.range_max;
|
||||
}
|
||||
|
||||
if (nodedata.name == "none") {
|
||||
nodedata.name.clear();
|
||||
} else {
|
||||
auto& id = world.AddComponent<ECSComponent::Identification>(entity);
|
||||
id.Name = nodedata.name;
|
||||
}
|
||||
} else {
|
||||
if (nodedata.name == "none")
|
||||
nodedata.name.clear();
|
||||
}
|
||||
|
||||
if( nodedata.name == "none" ) {
|
||||
nodedata.name.clear();
|
||||
} else {
|
||||
auto& id = world.AddComponent<ECSComponent::Identification>(entity);
|
||||
id.Name = nodedata.name;
|
||||
}
|
||||
// type-based deserialization. not elegant but it'll do
|
||||
if( nodedata.type == "dynamic" ) {
|
||||
|
||||
@@ -588,7 +677,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
||||
}
|
||||
scene::Groups.insert( scene::Groups.handle(), instance );
|
||||
simulation::Region->insert( instance );
|
||||
|
||||
|
||||
{
|
||||
entt::entity modelEntity = world.FindEntityByName(instance->name());
|
||||
if (modelEntity != entt::null) {
|
||||
@@ -601,7 +690,6 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
||||
t->Rotation = { instance->Angles().x, instance->Angles().y, instance->Angles().z, 1.0f };
|
||||
}
|
||||
|
||||
// smoke sources on static models
|
||||
if (instance->Model() != nullptr) {
|
||||
for (auto const &smokesource : instance->Model()->smoke_sources()) {
|
||||
auto& emitter = world.AddComponent<ECSComponent::ParticleEmitter>(modelEntity);
|
||||
@@ -609,6 +697,25 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
||||
emitter.isActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::string lname = instance->name();
|
||||
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
||||
bool isLamp = lname.find("lamp") != std::string::npos
|
||||
|| lname.find("latarni") != std::string::npos
|
||||
|| lname.find("reflektor") != std::string::npos
|
||||
|| lname.find("swiatlo") != std::string::npos
|
||||
|| lname.find("light") != std::string::npos;
|
||||
if (isLamp && !world.HasComponent<ECSComponent::SpotLight>(modelEntity)) {
|
||||
auto& spot = world.AddComponent<ECSComponent::SpotLight>(modelEntity);
|
||||
spot.Color = glm::vec3(1.0f, 0.92f, 0.75f);
|
||||
spot.Intensity = 1.5f;
|
||||
spot.Range = 40.0f;
|
||||
spot.InnerAngle = 20.0f;
|
||||
spot.OuterAngle = 45.0f;
|
||||
spot.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#include "utilities/parser.h"
|
||||
#include "scene/scene.h"
|
||||
#include "utilities/AsyncFilePreloader.h"
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
namespace simulation {
|
||||
|
||||
@@ -23,8 +27,23 @@ struct deserializer_state {
|
||||
std::string,
|
||||
deserializefunctionbind> functionmap;
|
||||
|
||||
// background thread that scans the scenario ahead of the main parse and
|
||||
// queues model files for asynchronous disk preloading
|
||||
std::thread prefetch_thread;
|
||||
std::atomic<bool> prefetch_cancel { false };
|
||||
|
||||
deserializer_state(std::string const &File, cParser::buffertype const Type, const std::string &Path, bool const Loadtraction)
|
||||
: scenariofile(File), input(File, Type, Path, Loadtraction) { }
|
||||
|
||||
~deserializer_state() {
|
||||
// safety net for the aborted-load path; on success deserialize_continue
|
||||
// has already joined and stopped the preloader (idempotent here)
|
||||
prefetch_cancel = true;
|
||||
if (prefetch_thread.joinable())
|
||||
prefetch_thread.join();
|
||||
GModelPreloader.stop();
|
||||
GModelPreloader.clear();
|
||||
}
|
||||
};
|
||||
|
||||
class state_serializer {
|
||||
|
||||
86
utilities/AsyncFilePreloader.cpp
Normal file
86
utilities/AsyncFilePreloader.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "stdafx.h"
|
||||
#include "AsyncFilePreloader.h"
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
AsyncFilePreloader GModelPreloader;
|
||||
|
||||
void AsyncFilePreloader::start(int thread_count) {
|
||||
if (m_running.load()) return;
|
||||
|
||||
int n = thread_count > 0 ? thread_count
|
||||
: (int)std::thread::hardware_concurrency();
|
||||
n = std::clamp(n, 1, 8);
|
||||
|
||||
m_running = true;
|
||||
m_workers.reserve(n);
|
||||
for (int i = 0; i < n; ++i)
|
||||
m_workers.emplace_back([this] { worker(); });
|
||||
}
|
||||
|
||||
void AsyncFilePreloader::stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
m_running = false;
|
||||
// drain the queue so workers don't pick up more work
|
||||
while (!m_pending.empty()) m_pending.pop();
|
||||
}
|
||||
m_cv.notify_all();
|
||||
for (auto& t : m_workers)
|
||||
if (t.joinable()) t.join();
|
||||
m_workers.clear();
|
||||
}
|
||||
|
||||
void AsyncFilePreloader::queue(std::string path) {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
if (!m_running) return;
|
||||
if (!m_queued.insert(path).second) return; // already queued / cached
|
||||
m_pending.push(std::move(path));
|
||||
m_cv.notify_one();
|
||||
}
|
||||
|
||||
AsyncFilePreloader::Buffer AsyncFilePreloader::get(const std::string& path) const {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
auto it = m_cache.find(path);
|
||||
return (it != m_cache.end()) ? it->second : nullptr;
|
||||
}
|
||||
|
||||
void AsyncFilePreloader::clear() {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
m_cache.clear();
|
||||
m_queued.clear();
|
||||
}
|
||||
|
||||
void AsyncFilePreloader::worker() {
|
||||
while (true) {
|
||||
std::string path;
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(m_mutex);
|
||||
m_cv.wait(lk, [this] {
|
||||
return !m_pending.empty() || !m_running.load(std::memory_order_relaxed);
|
||||
});
|
||||
if (!m_running && m_pending.empty()) return;
|
||||
if (m_pending.empty()) continue;
|
||||
path = std::move(m_pending.front());
|
||||
m_pending.pop();
|
||||
}
|
||||
|
||||
auto buf = std::make_shared<std::vector<char>>();
|
||||
{
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (f) {
|
||||
auto sz = static_cast<std::streamsize>(f.tellg());
|
||||
f.seekg(0);
|
||||
buf->resize(static_cast<size_t>(sz));
|
||||
f.read(buf->data(), sz);
|
||||
if (!f) buf->clear(); // read error — fall back to sync load
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
m_cache[path] = std::move(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
utilities/AsyncFilePreloader.h
Normal file
57
utilities/AsyncFilePreloader.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#include <queue>
|
||||
#include <atomic>
|
||||
|
||||
// Thread pool that reads files from disk into memory so the main thread can
|
||||
// skip disk I/O and parse directly from RAM. Only covers the binary path
|
||||
// (E3D); text models fall back to synchronous load.
|
||||
class AsyncFilePreloader {
|
||||
public:
|
||||
using Buffer = std::shared_ptr<std::vector<char>>;
|
||||
|
||||
AsyncFilePreloader() = default;
|
||||
~AsyncFilePreloader() { stop(); }
|
||||
|
||||
AsyncFilePreloader(const AsyncFilePreloader&) = delete;
|
||||
AsyncFilePreloader& operator=(const AsyncFilePreloader&) = delete;
|
||||
|
||||
// Start N worker threads (clamped to [1, hardware_concurrency]).
|
||||
void start(int thread_count = 0);
|
||||
|
||||
// Signal workers to stop and join them. Clears the queue but keeps
|
||||
// already-loaded buffers so late callers still get cache hits.
|
||||
void stop();
|
||||
|
||||
// Queue a file for background loading. Silently skips duplicates.
|
||||
void queue(std::string path);
|
||||
|
||||
// Return the preloaded buffer for `path`, or nullptr if not ready yet.
|
||||
// Does NOT block — callers must fall back to synchronous I/O if nullptr.
|
||||
Buffer get(const std::string& path) const;
|
||||
|
||||
// Drop all cached buffers (call after loading finishes to free memory).
|
||||
void clear();
|
||||
|
||||
bool is_running() const { return m_running.load(std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
void worker();
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::condition_variable m_cv;
|
||||
std::queue<std::string> m_pending;
|
||||
std::unordered_set<std::string> m_queued; // prevents duplicate queuing
|
||||
std::unordered_map<std::string, Buffer> m_cache;
|
||||
std::vector<std::thread> m_workers;
|
||||
std::atomic<bool> m_running { false };
|
||||
};
|
||||
|
||||
extern AsyncFilePreloader GModelPreloader;
|
||||
Reference in New Issue
Block a user