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 {
using EntityID = uint64_t;
enum class EntityType { CREATURE, ITEM };
struct Entity {
EntityID id;
explicit Entity(EntityID id) : id(id) {}
EntityType type;
Entity(EntityID id, EntityType type) : id(id), type(type) {}
};
} // namespace Cubed

View File

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

View File

@@ -98,6 +98,8 @@ public:
uint32_t get_chunk_ref_count(const glm::vec3& pos) const;
ServerEntityManager& entity_manager();
std::shared_ptr<ThreadPool> get_compute_pool();
size_t player_sum() const;
int get_block(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;

View File

@@ -42,6 +42,8 @@ public:
bool is_recording() const;
RunMode runmode() const;
private:
enum class PauseUI { PAUSE_MENU, INVENTORY };
SceneManager& m_scene_manager;
@@ -61,6 +63,7 @@ private:
ErrorUI m_error_ui;
const Argument& m_argument;
VoiceInputType m_input_type;
RunMode m_runmode = RunMode::HYBRID;
bool handle_mouse_move_event(const MouseMoveEvent& e) override;
bool handle_mouse_button_event(const MouseButtonEvent& 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 client_pool = get_client_threads(mode);
auto client = get_client_threads(mode) + CLIENT_RESERVED_THREADS;
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, client_pool);
remain -= std::min(remain, client);
return std::max<size_t>(1, remain);
}

View File

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

View File

@@ -343,17 +343,20 @@ void DevPanel::show_world_tab_item() {
if (ImGui::BeginTabItem("world")) {
if (ImGui::BeginTabBar("World Kind")) {
auto& param = m_world_scene.scene_manager().world_scene_param();
if (param.host_game) {
auto mode = m_world_scene.runmode();
if (mode == RunMode::HYBRID || mode == RunMode::SERVER_ONLY) {
if (ImGui::BeginTabItem("ServerWorld")) {
show_server_world_table_bar();
ImGui::EndTabItem();
}
}
if (ImGui::BeginTabItem("Client World")) {
show_client_world_table_bar();
ImGui::EndTabItem();
if (mode == RunMode::HYBRID || mode == RunMode::CLIENT_ONLY) {
if (ImGui::BeginTabItem("Client World")) {
show_client_world_table_bar();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
@@ -366,47 +369,49 @@ void DevPanel::show_world_tab_item() {
}
void DevPanel::show_server_world_table_bar() {
auto& world = m_world_scene.server_world();
ImGui::Text("ChunkGenerator Seed: %u", ChunkGenerator::seed());
ImGui::Text("Pool Threads %d Max Support Threads %d Reserved Threads %d",
m_world_scene.server_world().gen_pool_threads(),
m_world_scene.server_world().max_threads(), RESERVED_THREADS);
ImGui::SliderInt("Set Pool Threads", &m_threads, 1,
m_world_scene.server_world().max_threads());
world.gen_pool_threads(), world.max_threads(),
RESERVED_THREADS);
ImGui::SliderInt("Set Pool Threads", &m_threads, 1, world.max_threads());
ImGui::SameLine();
if (ImGui::Button("Set")) {
m_world_scene.server_world().change_pool_threads(
ServerWorld::ThreadPoolKind::GEN, m_threads);
world.change_pool_threads(ServerWorld::ThreadPoolKind::GEN, m_threads);
}
if (m_threads >
m_world_scene.server_world().max_threads() - RESERVED_THREADS) {
if (m_threads > world.max_threads() - RESERVED_THREADS) {
ImGui::TextColored(
ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
"Waring: When the threads in the thread pool exceed \n(maximum "
"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,
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")) {
m_world_scene.server_world().need_gen(m_player->get_uuid());
world.need_gen(m_player->get_uuid());
}
ImGui::SameLine();
if (ImGui::Checkbox("Gen Thread", &m_gen_thread_running)) {
if (m_gen_thread_running) {
m_world_scene.server_world().start_gen_thread();
world.start_gen_thread();
} else {
m_world_scene.server_world().stop_gen_thread();
world.stop_gen_thread();
}
}
ImGui::Text("Server Chunk Size %d",
m_world_scene.server_world().chunk_size());
ImGui::Text("Server Chunk Size %d", 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::BeginTabItem("Time")) {
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(
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,
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) {
BaseClientCreature c;
c.model = ModelManager::instance().get_model_id("cubed:pig");
create_entity_in_registry(id, Entity{id}, EntityInfo{"cubed:pig", ""},
std::move(c), PigTag{}, RenderTransform{});
create_entity_in_registry(id, Entity{id, EntityType::CREATURE},
EntityInfo{"cubed:pig", ""}, std::move(c),
PigTag{}, RenderTransform{});
});
}
// not thread safe

View File

@@ -27,8 +27,8 @@ void ServerEntityManager::init() {
c.movement.deceleration = PigDefaults::DECELERATION;
c.velocity.max.x = c.velocity.max.z = PigDefaults::MAX_SPEED;
return create_entity_in_factory(
Entity{m_next}, EntityInfo{"cubed:pig", ""}, std::move(c), PigTag{},
AIBase{}, WanderAITag{}, MoveBoost{});
Entity{m_next, EntityType::CREATURE}, EntityInfo{"cubed:pig", ""},
std::move(c), PigTag{}, AIBase{}, WanderAITag{}, MoveBoost{});
});
}
void ServerEntityManager::update() {
@@ -134,8 +134,13 @@ void ServerEntityManager::handle_task() {
}
}
void ServerEntityManager::add_entity(std::string_view name,
const glm::vec3& world_pos) {
void ServerEntityManager::add_creature(std::string_view name,
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,
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));
}
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,
const glm::vec3& pos) {
ASSERT(m_factories.contains(name));
@@ -159,6 +173,7 @@ void ServerEntityManager::create_entity(std::string_view name,
ASSERT(t);
t->transform.position.value = 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) {
if (m_entity_sum == 0) {
Logger::error("entity sum is 0!");
return;
}
acc a;
if (!m_entities.find(a, id)) {
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_entities.erase(a);
--m_entity_sum;
auto sessions = m_world.get_all_session();
Arena 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) {
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) {
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() {
return m_compute_thread_pool.load();
}
size_t ServerWorld::player_sum() const { return m_player_sum.load(); }
} // 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")
: m_renderer.get_shader("model_instance");
glm::mat4 view = camera.get_camera_lookat();
shader.set_loc("proj_matrix", m_renderer.p_mat());
if (shadow) {
} else {
shader.set_loc("proj_matrix", m_renderer.p_mat());
shader.set_loc("view_matrix", view);
}
for (const auto& entry : batch.entries) {

View File

@@ -4,6 +4,7 @@
#include "Cubed/debug_collector.hpp"
#include "Cubed/gameplay/client_world.hpp"
#include "Cubed/gameplay/ecs/client_entity.hpp"
#include "Cubed/gameplay/hitbox_manager.hpp"
#include "Cubed/render/renderer.hpp"
#include "Cubed/render/renderer_constants.hpp"
#include "Cubed/scene/world_scene.hpp"
@@ -15,28 +16,48 @@
namespace Cubed {
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>>
instances_data_map;
auto& registry = world.entity_manager().get_registry();
auto view = registry.view<BaseClientCreature, RenderTransform>();
auto view =
registry.view<BaseClientCreature, RenderTransform, EntityInfo>();
for (auto entity : view) {
auto& creature = view.get<BaseClientCreature>(entity);
auto pos = creature.transform.position.value;
if (!world.is_render(pos)) {
continue;
}
auto& info = view.get<EntityInfo>(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);
ModelRender::InstanceData data;
data.pos = t.position.value;
data.yaw = yaw;
data.pose = creature.pose;
instances_data_map[creature.model].emplace_back(std::move(data));
++cnt;
// m_renderer.model_renderer().shadow_pass(
// creature.model, t.position.value, yaw,
// world.world_scene().camera(), creature.pose);
}
d_rep("rendered_entities", "Rendered Entities: {}", cnt);
return instances_data_map;
};
@@ -131,7 +152,7 @@ void WorldRenderer::day_night_calculation(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) {
m_renderer.model_renderer().build_vertices(id, data);
}

View File

@@ -129,21 +129,20 @@ void WorldScene::on_enter() {
load_config();
m_error_ui.init();
m_client = std::make_shared<NetworkClient>(m_client_world);
RunMode mode = RunMode::HYBRID;
if (m_argument.direct_enter) {
if (m_argument.ip) {
mode = RunMode::CLIENT_ONLY;
m_runmode = RunMode::CLIENT_ONLY;
}
} else {
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.ip) {
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);
} else {
m_client->start(*m_argument.ip, *m_argument.port);
@@ -159,7 +158,7 @@ void WorldScene::on_enter() {
} else {
ChunkGenerator::init();
}
m_server.start_server(param.port, mode);
m_server.start_server(param.port, m_runmode);
}
m_client->start(param.ip, param.port);
@@ -169,7 +168,7 @@ void WorldScene::on_enter() {
try {
m_client_world.init(m_argument.player.value_or("Unknown"), m_client,
mode);
m_runmode);
Logger::info("World Init Success");
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 {
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) {
Logger::error("WorldScene Error Set {}", error);
m_error_ui.set_error(error);