3 Commits

Author SHA1 Message Date
9745ccc7d6 perf(render): frustum cull entities before instance building
Add frustum culling to entity instance data collection using AABB vs camera frustum planes. Pass renderer into get_instances_data_map to compute MVP and extract planes. Also add "Rendered Entities" debug counter for visibility.
2026-08-06 20:40:01 +08:00
94d92f2159 feat(ecs): add entity type separation and creature limits
Replace generic entity creation with typed creatures and items. Rename `add_entity` to `add_creature`, enforce per-player creature cap, and track entity/creature totals for metrics. Expose run mode to dev panel and show new counts.
2026-08-06 20:13:55 +08:00
33cfea1f36 fix: correct thread count and shadow projection uniform
- Add reserved client thread count to client threads in server generation calculation.
- Set projection matrix uniform only for non-shadow rendering.
2026-08-06 17:17:29 +08:00
14 changed files with 121 additions and 44 deletions

View File

@@ -3,9 +3,12 @@
namespace Cubed { namespace Cubed {
using EntityID = uint64_t; using EntityID = uint64_t;
enum class EntityType { CREATURE, ITEM };
struct Entity { struct Entity {
EntityID id; EntityID id;
explicit Entity(EntityID id) : id(id) {} EntityType type;
Entity(EntityID id, EntityType type) : id(id), type(type) {}
}; };
} // namespace Cubed } // namespace Cubed

View File

@@ -12,15 +12,20 @@ class ServerWorld;
class Session; class Session;
class ServerEntityManager { class ServerEntityManager {
public: public:
static constexpr size_t PER_CREATURE_LIMITS = 100;
ServerEntityManager(ServerWorld& world); ServerEntityManager(ServerWorld& world);
void init(); void init();
void update(); void update();
// not thread safe void add_creature(std::string_view name, const glm::vec3& world_pos);
void add_entity(std::string_view name, const glm::vec3& world_pos);
void destory(EntityID id); void destory(EntityID id);
void handle_player_login(std::shared_ptr<Session> session); void handle_player_login(std::shared_ptr<Session> session);
size_t max_creature_sum() const;
size_t creature_sum() const;
size_t entity_sum() const;
private: private:
enum class Command { CREATE, SEND_ALL_ENTITIES, DESTORY }; enum class Command { CREATE, SEND_ALL_ENTITIES, DESTORY };
struct EntityCreateElement { struct EntityCreateElement {
@@ -43,6 +48,8 @@ private:
std::variant<std::shared_ptr<Session>, EntityCreateElement, EntityID>; std::variant<std::shared_ptr<Session>, EntityCreateElement, EntityID>;
using TaskPair = std::pair<Command, TaskElement>; using TaskPair = std::pair<Command, TaskElement>;
ServerWorld& m_world; ServerWorld& m_world;
std::atomic<size_t> m_creature_sum{0};
std::atomic<size_t> m_entity_sum{0};
tbb::concurrent_queue<TaskPair> m_tasks; tbb::concurrent_queue<TaskPair> m_tasks;
entt::registry m_registry; entt::registry m_registry;
EntityID m_next = 0; EntityID m_next = 0;

View File

@@ -98,6 +98,8 @@ public:
uint32_t get_chunk_ref_count(const glm::vec3& pos) const; uint32_t get_chunk_ref_count(const glm::vec3& pos) const;
ServerEntityManager& entity_manager(); ServerEntityManager& entity_manager();
std::shared_ptr<ThreadPool> get_compute_pool(); std::shared_ptr<ThreadPool> get_compute_pool();
size_t player_sum() const;
int get_block(const glm::ivec3& block_pos) const override; int get_block(const glm::ivec3& block_pos) const override;
bool is_solid(const glm::ivec3& block_pos) const override; bool is_solid(const glm::ivec3& block_pos) const override;
bool can_pass_block(const glm::ivec3& block_pos) const override; bool can_pass_block(const glm::ivec3& block_pos) const override;

View File

@@ -42,6 +42,8 @@ public:
bool is_recording() const; bool is_recording() const;
RunMode runmode() const;
private: private:
enum class PauseUI { PAUSE_MENU, INVENTORY }; enum class PauseUI { PAUSE_MENU, INVENTORY };
SceneManager& m_scene_manager; SceneManager& m_scene_manager;
@@ -61,6 +63,7 @@ private:
ErrorUI m_error_ui; ErrorUI m_error_ui;
const Argument& m_argument; const Argument& m_argument;
VoiceInputType m_input_type; VoiceInputType m_input_type;
RunMode m_runmode = RunMode::HYBRID;
bool handle_mouse_move_event(const MouseMoveEvent& e) override; bool handle_mouse_move_event(const MouseMoveEvent& e) override;
bool handle_mouse_button_event(const MouseButtonEvent& e) override; bool handle_mouse_button_event(const MouseButtonEvent& e) override;
bool handle_window_resize_event(const WindowResizeEvent& e) override; bool handle_window_resize_event(const WindowResizeEvent& e) override;

View File

@@ -110,7 +110,7 @@ inline size_t get_server_gen_threads(RunMode mode) {
auto update_pool = get_server_compute_treads(mode); auto update_pool = get_server_compute_treads(mode);
auto client_pool = get_client_threads(mode); auto client = get_client_threads(mode) + CLIENT_RESERVED_THREADS;
size_t remain = available; size_t remain = available;
@@ -118,7 +118,7 @@ inline size_t get_server_gen_threads(RunMode mode) {
remain -= std::min(remain, update_pool); remain -= std::min(remain, update_pool);
remain -= std::min(remain, client_pool); remain -= std::min(remain, client);
return std::max<size_t>(1, remain); return std::max<size_t>(1, remain);
} }

View File

@@ -68,6 +68,9 @@ void DebugCollector::init(int, int) {
// rendered_chunk // rendered_chunk
add_label("Rendered Chunk: 0", "rendered_chunk"); add_label("Rendered Chunk: 0", "rendered_chunk");
// rendered_entities
add_label("Rendered Entities: 0", "rendered_entities");
// rss // rss
add_label("RSS: 0mb", "rss"); add_label("RSS: 0mb", "rss");

View File

@@ -343,17 +343,20 @@ void DevPanel::show_world_tab_item() {
if (ImGui::BeginTabItem("world")) { if (ImGui::BeginTabItem("world")) {
if (ImGui::BeginTabBar("World Kind")) { if (ImGui::BeginTabBar("World Kind")) {
auto& param = m_world_scene.scene_manager().world_scene_param(); auto mode = m_world_scene.runmode();
if (param.host_game) { if (mode == RunMode::HYBRID || mode == RunMode::SERVER_ONLY) {
if (ImGui::BeginTabItem("ServerWorld")) { if (ImGui::BeginTabItem("ServerWorld")) {
show_server_world_table_bar(); show_server_world_table_bar();
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
} }
if (mode == RunMode::HYBRID || mode == RunMode::CLIENT_ONLY) {
if (ImGui::BeginTabItem("Client World")) { if (ImGui::BeginTabItem("Client World")) {
show_client_world_table_bar(); show_client_world_table_bar();
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
}
ImGui::EndTabBar(); ImGui::EndTabBar();
} }
@@ -366,47 +369,49 @@ void DevPanel::show_world_tab_item() {
} }
void DevPanel::show_server_world_table_bar() { void DevPanel::show_server_world_table_bar() {
auto& world = m_world_scene.server_world();
ImGui::Text("ChunkGenerator Seed: %u", ChunkGenerator::seed()); ImGui::Text("ChunkGenerator Seed: %u", ChunkGenerator::seed());
ImGui::Text("Pool Threads %d Max Support Threads %d Reserved Threads %d", ImGui::Text("Pool Threads %d Max Support Threads %d Reserved Threads %d",
m_world_scene.server_world().gen_pool_threads(), world.gen_pool_threads(), world.max_threads(),
m_world_scene.server_world().max_threads(), RESERVED_THREADS); RESERVED_THREADS);
ImGui::SliderInt("Set Pool Threads", &m_threads, 1, ImGui::SliderInt("Set Pool Threads", &m_threads, 1, world.max_threads());
m_world_scene.server_world().max_threads());
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Set")) { if (ImGui::Button("Set")) {
m_world_scene.server_world().change_pool_threads( world.change_pool_threads(ServerWorld::ThreadPoolKind::GEN, m_threads);
ServerWorld::ThreadPoolKind::GEN, m_threads);
} }
if (m_threads > if (m_threads > world.max_threads() - RESERVED_THREADS) {
m_world_scene.server_world().max_threads() - RESERVED_THREADS) {
ImGui::TextColored( ImGui::TextColored(
ImVec4(1.0f, 1.0f, 0.0f, 1.0f), ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
"Waring: When the threads in the thread pool exceed \n(maximum " "Waring: When the threads in the thread pool exceed \n(maximum "
"threads minus reserved threads), \nit may cause stuttering."); "threads minus reserved threads), \nit may cause stuttering.");
} }
m_chunk_style = m_world_scene.server_world().chunk_load_style(); m_chunk_style = world.chunk_load_style();
if (ImGui::Combo("ChunkLoadStyle", &m_chunk_style, CHUNK_LOAD_STYLE, if (ImGui::Combo("ChunkLoadStyle", &m_chunk_style, CHUNK_LOAD_STYLE,
IM_ARRAYSIZE(CHUNK_LOAD_STYLE))) { IM_ARRAYSIZE(CHUNK_LOAD_STYLE))) {
m_world_scene.server_world().set_chunk_load_style(m_chunk_style); world.set_chunk_load_style(m_chunk_style);
} }
if (ImGui::Button("Request Chunk Build")) { if (ImGui::Button("Request Chunk Build")) {
m_world_scene.server_world().need_gen(m_player->get_uuid()); world.need_gen(m_player->get_uuid());
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Checkbox("Gen Thread", &m_gen_thread_running)) { if (ImGui::Checkbox("Gen Thread", &m_gen_thread_running)) {
if (m_gen_thread_running) { if (m_gen_thread_running) {
m_world_scene.server_world().start_gen_thread(); world.start_gen_thread();
} else { } else {
m_world_scene.server_world().stop_gen_thread(); world.stop_gen_thread();
} }
} }
ImGui::Text("Server Chunk Size %d", ImGui::Text("Server Chunk Size %d", world.chunk_size());
m_world_scene.server_world().chunk_size()); ImGui::SameLine();
ImGui::Text("Server Player Sum %zu", world.player_sum());
ImGui::Text("Server Entity Sum %zu", world.entity_manager().entity_sum());
ImGui::SameLine();
ImGui::Text("Server Creature Sum %zu",
world.entity_manager().creature_sum());
if (ImGui::BeginTabBar("World Settings")) { if (ImGui::BeginTabBar("World Settings")) {
if (ImGui::BeginTabItem("Time")) { if (ImGui::BeginTabItem("Time")) {
show_time_table_bar(); show_time_table_bar();

View File

@@ -829,7 +829,7 @@ void ChunkGenerator::spawn_creature() {
} }
auto [world_x, world_y, world_z] = Chunk::block_to_world( auto [world_x, world_y, world_z] = Chunk::block_to_world(
x, y + 1, z, chunk_pos.x, chunk_pos.z); x, y + 1, z, chunk_pos.x, chunk_pos.z);
m_chunk.world().entity_manager().add_entity( m_chunk.world().entity_manager().add_creature(
SpawnDefaults::PIG.name, SpawnDefaults::PIG.name,
glm::vec3{world_x, world_y, world_z}); glm::vec3{world_x, world_y, world_z});
} }

View File

@@ -31,8 +31,9 @@ void ClientEntityManager::init() {
m_factories.emplace("cubed:pig", [this](EntityID id) { m_factories.emplace("cubed:pig", [this](EntityID id) {
BaseClientCreature c; BaseClientCreature c;
c.model = ModelManager::instance().get_model_id("cubed:pig"); c.model = ModelManager::instance().get_model_id("cubed:pig");
create_entity_in_registry(id, Entity{id}, EntityInfo{"cubed:pig", ""}, create_entity_in_registry(id, Entity{id, EntityType::CREATURE},
std::move(c), PigTag{}, RenderTransform{}); EntityInfo{"cubed:pig", ""}, std::move(c),
PigTag{}, RenderTransform{});
}); });
} }
// not thread safe // not thread safe

View File

@@ -27,8 +27,8 @@ void ServerEntityManager::init() {
c.movement.deceleration = PigDefaults::DECELERATION; c.movement.deceleration = PigDefaults::DECELERATION;
c.velocity.max.x = c.velocity.max.z = PigDefaults::MAX_SPEED; c.velocity.max.x = c.velocity.max.z = PigDefaults::MAX_SPEED;
return create_entity_in_factory( return create_entity_in_factory(
Entity{m_next}, EntityInfo{"cubed:pig", ""}, std::move(c), PigTag{}, Entity{m_next, EntityType::CREATURE}, EntityInfo{"cubed:pig", ""},
AIBase{}, WanderAITag{}, MoveBoost{}); std::move(c), PigTag{}, AIBase{}, WanderAITag{}, MoveBoost{});
}); });
} }
void ServerEntityManager::update() { void ServerEntityManager::update() {
@@ -134,8 +134,13 @@ void ServerEntityManager::handle_task() {
} }
} }
void ServerEntityManager::add_entity(std::string_view name, void ServerEntityManager::add_creature(std::string_view name,
const glm::vec3& world_pos) { const glm::vec3& world_pos) {
if (m_creature_sum.fetch_add(1) >= max_creature_sum()) {
m_creature_sum.fetch_sub(1);
return;
}
++m_entity_sum;
m_tasks.emplace(Command::CREATE, m_tasks.emplace(Command::CREATE,
EntityCreateElement{std::string(name), world_pos}); EntityCreateElement{std::string(name), world_pos});
} }
@@ -149,6 +154,15 @@ void ServerEntityManager::handle_player_login(
m_tasks.emplace(Command::SEND_ALL_ENTITIES, std::move(session)); m_tasks.emplace(Command::SEND_ALL_ENTITIES, std::move(session));
} }
size_t ServerEntityManager::max_creature_sum() const {
return PER_CREATURE_LIMITS * m_world.player_sum();
}
size_t ServerEntityManager::creature_sum() const {
return m_creature_sum.load();
}
size_t ServerEntityManager::entity_sum() const { return m_entity_sum.load(); }
void ServerEntityManager::create_entity(std::string_view name, void ServerEntityManager::create_entity(std::string_view name,
const glm::vec3& pos) { const glm::vec3& pos) {
ASSERT(m_factories.contains(name)); ASSERT(m_factories.contains(name));
@@ -159,6 +173,7 @@ void ServerEntityManager::create_entity(std::string_view name,
ASSERT(t); ASSERT(t);
t->transform.position.value = pos; t->transform.position.value = pos;
} }
handle_entity_create(e, name, pos); handle_entity_create(e, name, pos);
} }
@@ -193,12 +208,23 @@ void ServerEntityManager::handle_entity_create(EntityID id,
} }
void ServerEntityManager::handle_entity_destory(EntityID id) { void ServerEntityManager::handle_entity_destory(EntityID id) {
if (m_entity_sum == 0) {
Logger::error("entity sum is 0!");
return;
}
acc a; acc a;
if (!m_entities.find(a, id)) { if (!m_entities.find(a, id)) {
return; return;
} }
auto e = m_registry.try_get<Entity>(a->second);
ASSERT(e);
if (e->type == EntityType::CREATURE) {
--m_creature_sum;
}
m_registry.destroy(a->second); m_registry.destroy(a->second);
m_entities.erase(a); m_entities.erase(a);
--m_entity_sum;
auto sessions = m_world.get_all_session(); auto sessions = m_world.get_all_session();
Arena arena; Arena arena;
auto* s2c = Arena::Create<S2CEntityDestory>(&arena); auto* s2c = Arena::Create<S2CEntityDestory>(&arena);

View File

@@ -896,7 +896,7 @@ void ServerWorld::handle_block_change(const BlockChangeReq& req) {
void ServerWorld::handle_entity_create(C2SEntityCreateRequest& req) { void ServerWorld::handle_entity_create(C2SEntityCreateRequest& req) {
m_entity_manager.add_entity(req.name(), Tools::get_net_vec3(req.pos())); m_entity_manager.add_creature(req.name(), Tools::get_net_vec3(req.pos()));
} }
void ServerWorld::handle_entity_destory(C2SEntityDestoryRequest& req) { void ServerWorld::handle_entity_destory(C2SEntityDestoryRequest& req) {
m_entity_manager.destory(req.id()); m_entity_manager.destory(req.id());
@@ -1133,4 +1133,7 @@ ServerEntityManager& ServerWorld::entity_manager() { return m_entity_manager; }
std::shared_ptr<ThreadPool> ServerWorld::get_compute_pool() { std::shared_ptr<ThreadPool> ServerWorld::get_compute_pool() {
return m_compute_thread_pool.load(); return m_compute_thread_pool.load();
} }
size_t ServerWorld::player_sum() const { return m_player_sum.load(); }
} // namespace Cubed } // namespace Cubed

View File

@@ -76,9 +76,10 @@ void ModelRender::render_instance(ModelID id, size_t sum, const Camera& camera,
auto& shader = shadow ? m_renderer.get_shader("depth_model_instance") auto& shader = shadow ? m_renderer.get_shader("depth_model_instance")
: m_renderer.get_shader("model_instance"); : m_renderer.get_shader("model_instance");
glm::mat4 view = camera.get_camera_lookat(); glm::mat4 view = camera.get_camera_lookat();
shader.set_loc("proj_matrix", m_renderer.p_mat());
if (shadow) { if (shadow) {
} else { } else {
shader.set_loc("proj_matrix", m_renderer.p_mat());
shader.set_loc("view_matrix", view); shader.set_loc("view_matrix", view);
} }
for (const auto& entry : batch.entries) { for (const auto& entry : batch.entries) {

View File

@@ -4,6 +4,7 @@
#include "Cubed/debug_collector.hpp" #include "Cubed/debug_collector.hpp"
#include "Cubed/gameplay/client_world.hpp" #include "Cubed/gameplay/client_world.hpp"
#include "Cubed/gameplay/ecs/client_entity.hpp" #include "Cubed/gameplay/ecs/client_entity.hpp"
#include "Cubed/gameplay/hitbox_manager.hpp"
#include "Cubed/render/renderer.hpp" #include "Cubed/render/renderer.hpp"
#include "Cubed/render/renderer_constants.hpp" #include "Cubed/render/renderer_constants.hpp"
#include "Cubed/scene/world_scene.hpp" #include "Cubed/scene/world_scene.hpp"
@@ -15,28 +16,48 @@
namespace Cubed { namespace Cubed {
namespace { namespace {
WorldRenderer::InstanceDataMap get_instances_data_map(ClientWorld& world) { WorldRenderer::InstanceDataMap get_instances_data_map(ClientWorld& world,
Renderer& renderer) {
glm::mat4 mvp_mat =
renderer.p_mat() * world.world_scene().camera().get_camera_lookat();
auto& m_planes = world.planes();
Math::extract_frustum_planes(mvp_mat, m_planes);
size_t cnt = 0;
std::unordered_map<ModelID, std::vector<ModelRender::InstanceData>> std::unordered_map<ModelID, std::vector<ModelRender::InstanceData>>
instances_data_map; instances_data_map;
auto& registry = world.entity_manager().get_registry(); auto& registry = world.entity_manager().get_registry();
auto view = registry.view<BaseClientCreature, RenderTransform>(); auto view =
registry.view<BaseClientCreature, RenderTransform, EntityInfo>();
for (auto entity : view) { for (auto entity : view) {
auto& creature = view.get<BaseClientCreature>(entity); auto& creature = view.get<BaseClientCreature>(entity);
auto pos = creature.transform.position.value; auto pos = creature.transform.position.value;
if (!world.is_render(pos)) { if (!world.is_render(pos)) {
continue; continue;
} }
auto& info = view.get<EntityInfo>(entity);
auto& t = view.get<RenderTransform>(entity); auto& t = view.get<RenderTransform>(entity);
auto aabb = HitboxManager::hitbox(info.name);
aabb.box.center += t.position.value;
if (!Math::is_aabb_in_frustum(aabb.box.center, aabb.box.half + 1.0f,
m_planes)) {
continue;
}
float yaw = std::atan2(t.direction.value.x, t.direction.value.z); float yaw = std::atan2(t.direction.value.x, t.direction.value.z);
ModelRender::InstanceData data; ModelRender::InstanceData data;
data.pos = t.position.value; data.pos = t.position.value;
data.yaw = yaw; data.yaw = yaw;
data.pose = creature.pose; data.pose = creature.pose;
instances_data_map[creature.model].emplace_back(std::move(data)); instances_data_map[creature.model].emplace_back(std::move(data));
++cnt;
// m_renderer.model_renderer().shadow_pass( // m_renderer.model_renderer().shadow_pass(
// creature.model, t.position.value, yaw, // creature.model, t.position.value, yaw,
// world.world_scene().camera(), creature.pose); // world.world_scene().camera(), creature.pose);
} }
d_rep("rendered_entities", "Rendered Entities: {}", cnt);
return instances_data_map; return instances_data_map;
}; };
@@ -131,7 +152,7 @@ void WorldRenderer::day_night_calculation(ClientWorld& world) {
} }
WorldRenderer::InstanceDataMap WorldRenderer::entity_build(ClientWorld& world) { WorldRenderer::InstanceDataMap WorldRenderer::entity_build(ClientWorld& world) {
auto instances_data_map = get_instances_data_map(world); auto instances_data_map = get_instances_data_map(world, m_renderer);
for (auto& [id, data] : instances_data_map) { for (auto& [id, data] : instances_data_map) {
m_renderer.model_renderer().build_vertices(id, data); m_renderer.model_renderer().build_vertices(id, data);
} }

View File

@@ -129,21 +129,20 @@ void WorldScene::on_enter() {
load_config(); load_config();
m_error_ui.init(); m_error_ui.init();
m_client = std::make_shared<NetworkClient>(m_client_world); m_client = std::make_shared<NetworkClient>(m_client_world);
RunMode mode = RunMode::HYBRID;
if (m_argument.direct_enter) { if (m_argument.direct_enter) {
if (m_argument.ip) { if (m_argument.ip) {
mode = RunMode::CLIENT_ONLY; m_runmode = RunMode::CLIENT_ONLY;
} }
} else { } else {
if (!m_scene_manager.world_scene_param().host_game) { if (!m_scene_manager.world_scene_param().host_game) {
mode = RunMode::CLIENT_ONLY; m_runmode = RunMode::CLIENT_ONLY;
} }
} }
if (m_argument.direct_enter) { if (m_argument.direct_enter) {
if (!m_argument.ip) { if (!m_argument.ip) {
ChunkGenerator::init(); ChunkGenerator::init();
m_server.start_server(*m_argument.port, mode); m_server.start_server(*m_argument.port, m_runmode);
m_client->start("127.0.0.1", *m_argument.port); m_client->start("127.0.0.1", *m_argument.port);
} else { } else {
m_client->start(*m_argument.ip, *m_argument.port); m_client->start(*m_argument.ip, *m_argument.port);
@@ -159,7 +158,7 @@ void WorldScene::on_enter() {
} else { } else {
ChunkGenerator::init(); ChunkGenerator::init();
} }
m_server.start_server(param.port, mode); m_server.start_server(param.port, m_runmode);
} }
m_client->start(param.ip, param.port); m_client->start(param.ip, param.port);
@@ -169,7 +168,7 @@ void WorldScene::on_enter() {
try { try {
m_client_world.init(m_argument.player.value_or("Unknown"), m_client, m_client_world.init(m_argument.player.value_or("Unknown"), m_client,
mode); m_runmode);
Logger::info("World Init Success"); Logger::info("World Init Success");
m_camera.camera_init(&m_client_world.get_player()); m_camera.camera_init(&m_client_world.get_player());
@@ -461,6 +460,9 @@ void WorldScene::handle_chat_message(ChatMessage& message) {
bool WorldScene::is_recording() const { bool WorldScene::is_recording() const {
return m_client_world.get_audio().audio_recording().is_recording(); return m_client_world.get_audio().audio_recording().is_recording();
} }
RunMode WorldScene::runmode() const { return m_runmode; }
void WorldScene::set_error(std::string_view error) { void WorldScene::set_error(std::string_view error) {
Logger::error("WorldScene Error Set {}", error); Logger::error("WorldScene Error Set {}", error);
m_error_ui.set_error(error); m_error_ui.set_error(error);