2 Commits

Author SHA1 Message Date
720f9fe0c1 build(windows): add Nvidia and AMD GPU high performance exports 2026-06-29 16:03:58 +08:00
85db85a737 refactor(client_world): use concurrent data structures for chunk management
Replace std::shared_mutex and std::unordered_set with tbb::concurrent_hash_map for chunk storage, tbb::concurrent_queue for pending uploads, and absl::flat_hash_set for player chunk positions. This improves thread safety and reduces locking overhead. Refactor receive_chunk and request_chunk to work with the new concurrent model.
2026-06-29 14:14:36 +08:00
7 changed files with 194 additions and 123 deletions

View File

@@ -6,6 +6,7 @@
#include "Cubed/gameplay/game_mode.hpp"
#include "Cubed/input.hpp"
#include <absl/container/flat_hash_set.h>
#include <glm/glm.hpp>
#include <optional>
#include <shared_mutex>
@@ -14,8 +15,14 @@ enum class Gait { WALK = 0, RUN };
class ClientWorld;
class ClientPlayer {
public:
using ChunkPosSet = absl::flat_hash_set<ChunkPos, ChunkPos::Hash>;
ClientPlayer(ClientWorld& world);
~ClientPlayer();
void update_chunk_set(const ChunkPosSet& set);
const ChunkPosSet& get_chunk_pos_set() const;
ChunkPosSet& get_chunk_pos_set();
AABB get_aabb(const glm::vec3& pos) const;
const glm::vec3& get_front() const;
const Gait& get_gait() const;
@@ -101,7 +108,8 @@ private:
ClientWorld& m_world;
mutable std::shared_mutex m_player_pos_mutex;
mutable std::shared_mutex m_chunk_pos_mutex;
ChunkPosSet m_player_chunk_pos_set;
bool ray_cast(const glm::vec3& start, const glm::vec3& dir,
glm::ivec3& block_pos, glm::vec3& normal,
float distance = 4.0f);

View File

@@ -7,7 +7,10 @@
#include "Cubed/gameplay/network_client.hpp"
#include "Cubed/tools/thread_pool.hpp"
#include <absl/container/flat_hash_set.h>
#include <deque>
#include <tbb/concurrent_hash_map.h>
#include <tbb/concurrent_queue.h>
#include <tbb/concurrent_unordered_map.h>
namespace Cubed {
@@ -64,10 +67,10 @@ public:
const std::vector<ChunkRenderSnapshot>& render_snapshots() const;
const std::vector<RemotePlayerRenderData>& render_player_data() const;
glm::vec3 sunlight_dir() const;
void receive_chunk(ChunkDataRsp data);
void receive_chunk(std::vector<uint8_t> data, PacketHeader header);
void request_exit();
bool is_receive_exit();
int chunk_size() const;
template <typename Fn>
void register_timer(std::string_view id, TickType threshold, Fn&& f) {
m_timers.emplace(std::piecewise_construct,
@@ -78,12 +81,14 @@ public:
private:
enum class ChunkLoadStyle { RANDOM, CENTER };
using ChunkHashMap =
tbb::concurrent_unordered_map<ChunkPos, ClientChunk, ChunkPos::Hash>;
using ChunkPosSet = std::unordered_set<ChunkPos, ChunkPos::Hash>;
tbb::concurrent_hash_map<ChunkPos, std::shared_ptr<ClientChunk>,
ChunkPos::TBBHash>;
using ChunkPosSet = absl::flat_hash_set<ChunkPos, ChunkPos::Hash>;
using ChunkPosVector = std::vector<ChunkPos>;
using OtherPlayerHashMap =
std::unordered_map<std::string, RemotePlayerInfo>;
using chunk_acc = ChunkHashMap::accessor;
using chunk_cacc = ChunkHashMap::const_accessor;
static constexpr int WORLD_EXIT_TIMEOUT = 200;
ClientPlayer m_player;
@@ -92,13 +97,11 @@ private:
std::vector<glm::vec4> m_planes;
std::jthread m_client_thread;
mutable std::shared_mutex m_chunks_mutex;
std::mutex m_delete_vbo_mutex;
std::mutex m_delete_vao_mutex;
std::mutex m_pending_upload_queue_mutex;
std::mutex m_other_players_mutex;
std::deque<ClientChunk> m_pending_upload_queue;
tbb::concurrent_queue<std::unique_ptr<ClientChunk>> m_pending_upload_queue;
std::vector<GLuint> m_pending_delete_vbo;
std::vector<GLuint> m_pending_delete_vao;
@@ -127,5 +130,7 @@ private:
void report_player_pos();
void set_block(const glm::ivec3& pos, unsigned id);
void update_chunk(const ChunkPosSet& old, const ChunkPosSet& now);
};
} // namespace Cubed

View File

@@ -576,6 +576,7 @@ void DevPanel::show_client_world_table_bar() {
m_player->set_player_pos({0.0f, 255.0f, 0.0f});
}
ImGui::Text("Chunk Task Id %d", m_app.client_world().get_chunk_task_id());
ImGui::Text("Client World Chunk %d", m_app.client_world().chunk_size());
}
void DevPanel::show_player_tab_item() {

View File

@@ -542,6 +542,22 @@ void ClientPlayer::update_scroll(double yoffset) {
}
}
void ClientPlayer::update_chunk_set(const ChunkPosSet& set) {
std::lock_guard lock(m_chunk_pos_mutex);
m_player_chunk_pos_set.clear();
m_player_chunk_pos_set.insert(set.begin(), set.end());
}
const ClientPlayer::ChunkPosSet& ClientPlayer::get_chunk_pos_set() const {
std::shared_lock lock(m_chunk_pos_mutex);
return m_player_chunk_pos_set;
}
ClientPlayer::ChunkPosSet& ClientPlayer::get_chunk_pos_set() {
std::lock_guard lock(m_chunk_pos_mutex);
return m_player_chunk_pos_set;
}
float& ClientPlayer::max_walk_speed() { return m_max_walk_speed; }
float& ClientPlayer::max_run_speed() { return m_max_run_speed; }
float& ClientPlayer::max_speed() { return m_max_speed; }

View File

@@ -23,10 +23,9 @@ ClientWorld::ClientWorld() : m_player(*this) {}
ClientWorld::~ClientWorld() {
stop_client_thread();
stop_thread_pool();
{
std::lock_guard lock(m_chunks_mutex);
m_chunks.clear();
}
m_chunks.clear();
{
std::lock_guard lk(m_delete_vbo_mutex);
for (auto x : m_pending_delete_vbo) {
@@ -53,14 +52,13 @@ ClientPlayer& ClientWorld::get_player() { return m_player; }
int ClientWorld::get_block(const glm::ivec3& block_pos) const {
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
std::shared_lock lk(m_chunks_mutex);
auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z});
chunk_cacc cacc;
if (it == m_chunks.end()) {
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
return 0;
}
const auto& chunk_blocks = it->second.get_chunk_blocks();
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE) {
@@ -70,13 +68,12 @@ int ClientWorld::get_block(const glm::ivec3& block_pos) const {
}
bool ClientWorld::is_solid(const glm::ivec3& block_pos) const {
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
std::shared_lock lk(m_chunks_mutex);
auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z});
chunk_cacc cacc;
if (it == m_chunks.end()) {
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
return false;
}
const auto& chunk_blocks = it->second.get_chunk_blocks();
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE) {
@@ -91,13 +88,12 @@ bool ClientWorld::is_solid(const glm::ivec3& block_pos) const {
}
bool ClientWorld::can_pass_block(const glm::ivec3& block_pos) const {
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
std::shared_lock lk(m_chunks_mutex);
auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z});
chunk_cacc cacc;
if (it == m_chunks.end()) {
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
return true;
}
const auto& chunk_blocks = it->second.get_chunk_blocks();
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE) {
@@ -111,31 +107,31 @@ void ClientWorld::rebuild_world() {
if (m_is_rebuilding.exchange(true)) {
return;
}
stop_client_thread();
stop_thread_pool();
{
std::lock_guard lk(m_chunks_mutex);
m_chunks.clear();
}
{
std::lock_guard lock(m_pending_upload_queue_mutex);
m_pending_upload_queue.clear();
}
m_chunks.clear();
m_pending_upload_queue.clear();
start_thread_pool();
start_client_thread(m_player.get_uuid());
request_chunk();
m_is_rebuilding = false;
}
BlockType ClientWorld::get_block_tpye(const glm::ivec3& block_pos) const {
auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z);
std::shared_lock lk(m_chunks_mutex);
auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z});
chunk_cacc cacc;
;
if (it == m_chunks.end()) {
if (!m_chunks.find(cacc, ChunkPos{chunk_x, chunk_z})) {
// Logger::error("Can't Find Block {} {} {}", block_pos.x, block_pos.y,
// block_pos.z);
return 0;
}
const auto& chunk_blocks = it->second.get_chunk_blocks();
const auto& chunk_blocks = cacc->second->get_chunk_blocks();
auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z});
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE) {
@@ -152,22 +148,23 @@ void ClientWorld::set_block(const glm::ivec3& block_pos, unsigned id) {
world_z = block_pos.z;
auto [chunk_x, chunk_z] = get_chunk_pos(world_x, world_z);
std::lock_guard lk(m_chunks_mutex);
auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z});
{
chunk_acc acc;
if (it == m_chunks.end()) {
return;
if (!m_chunks.find(acc, ChunkPos{chunk_x, chunk_z})) {
return;
}
auto [x, y, z] = ClientChunk::world_to_block(world_x, world_y, world_z,
chunk_x, chunk_z);
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE) {
return;
}
acc->second->set_chunk_block(ClientChunk::index(x, y, z), id);
}
auto [x, y, z] = ClientChunk::world_to_block(world_x, world_y, world_z,
chunk_x, chunk_z);
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE) {
return;
}
it->second.set_chunk_block(ClientChunk::index(x, y, z), id);
static const glm::ivec3 NEIGHBOR_DIRS[] = {
{1, 0, 0}, {-1, 0, 0}, {0, 0, -1}, {0, 0, 1}};
@@ -175,9 +172,11 @@ void ClientWorld::set_block(const glm::ivec3& block_pos, unsigned id) {
glm::ivec3 neighbor = block_pos + dir;
auto [cx, cz] = get_chunk_pos(neighbor.x, neighbor.z);
auto it = m_chunks.find({cx, cz});
if (it != m_chunks.end()) {
it->second.mark_dirty();
{
chunk_acc acc;
if (m_chunks.find(acc, {cx, cz})) {
acc->second->mark_dirty();
}
}
}
}
@@ -251,7 +250,6 @@ void ClientWorld::receive_player_logout(const LogoutRsp& rsp) {
void ClientWorld::init(std::string_view player_name,
std::shared_ptr<NetworkClient> client) {
m_chunks.reserve(MAX_DISTANCE * MAX_DISTANCE * 4);
m_player.init(player_name);
m_client = client;
// timer
@@ -361,6 +359,26 @@ void ClientWorld::report_player_pos() {
m_client->send(make_packet(*pos));
}
void ClientWorld::update_chunk(const ChunkPosSet& old, const ChunkPosSet& now) {
// Elements in the old set that are not contained in now are not needed by
// the current player.
for (auto& pos : old) {
if (!now.contains(pos)) {
chunk_acc acc;
if (!m_chunks.find(acc, pos)) {
Logger::warn("Update Ref Count Error, can't Find old pos "
"in m_chunks");
continue;
}
m_chunks.erase(acc);
}
}
}
void ClientWorld::request_chunk() {
if (m_requesting_chunk.exchange(true)) {
Logger::warn("It is requesting new chunk!");
@@ -386,24 +404,20 @@ void ClientWorld::request_chunk() {
}
}
ChunkPosVector need_send_pos;
{
std::lock_guard lk(m_chunks_mutex);
for (auto it = m_chunks.begin(); it != m_chunks.end();) {
if (required_chunks.find(it->first) == required_chunks.end()) {
it = m_chunks.unsafe_erase(it);
} else {
++it;
}
}
ChunkPosSet old = std::move(m_player.get_chunk_pos_set());
m_player.update_chunk_set(required_chunks);
for (auto pos : required_chunks) {
auto it = m_chunks.find(pos);
if (it == m_chunks.end()) {
need_send_pos.emplace_back(pos);
}
ChunkPosVector need_send_pos;
for (auto pos : required_chunks) {
chunk_cacc cacc;
if (!m_chunks.find(cacc, pos)) {
need_send_pos.emplace_back(pos);
}
}
update_chunk(old, required_chunks);
if (need_send_pos.empty()) {
m_requesting_chunk = false;
return;
@@ -416,9 +430,8 @@ void ClientWorld::request_chunk() {
case CENTER: {
glm::vec3 player_pos = m_player.get_player_pos();
auto dist2 = [player_pos](ChunkPos chunk_pos) {
ChunkPos player_chunk_pos =
get_chunk_pos(player_pos.x, player_pos.z);
ChunkPos player_chunk_pos = get_chunk_pos(player_pos.x, player_pos.z);
auto dist2 = [player_chunk_pos](ChunkPos chunk_pos) {
float dx = player_chunk_pos.x - chunk_pos.x;
float dz = player_chunk_pos.z - chunk_pos.z;
return dx * dx + dz * dz;
@@ -446,36 +459,48 @@ void ClientWorld::request_chunk() {
m_requesting_chunk = false;
}
void ClientWorld::receive_chunk(ChunkDataRsp data) {
if (data.task_id() < m_chunk_task_id) {
return;
}
void ClientWorld::receive_chunk(std::vector<uint8_t> raw_data,
PacketHeader header) {
{
std::lock_guard lock(m_chunks_mutex);
ChunkPos pos{data.pos().x(), data.pos().z()};
if (m_chunks.find(pos) != m_chunks.end()) {
Logger::warn("Chunk {} {} has already in client world", pos.x,
pos.z);
return;
}
}
// vertex data will genrator in client thread pool instead of net thread;
auto pool = m_thread_pool.load();
if (!pool) {
Logger::error("Client Thread Pool is nullptr");
return;
}
pool->enqueue([this, data = std::move(data)]() {
ClientChunk chunk{*this};
chunk.receive_chunk(data);
{
std::lock_guard lock(m_pending_upload_queue_mutex);
m_pending_upload_queue.emplace_back(std::move(chunk));
}
});
pool->enqueue(
[this, raw_data = std::move(raw_data), header = std::move(header)]() {
Arena arena;
auto* data = Arena::Create<ChunkDataRsp>(&arena);
if (!decode_packet(*data, raw_data, header)) {
return;
}
if (data->task_id() < m_chunk_task_id) {
return;
}
{
chunk_cacc cacc;
ChunkPos pos{data->pos().x(), data->pos().z()};
if (m_chunks.find(cacc, pos)) {
Logger::warn("Chunk {} {} has already in client world",
pos.x, pos.z);
return;
}
}
std::unique_ptr<ClientChunk> chunk =
std::make_unique<ClientChunk>(*this);
chunk->receive_chunk(*data);
m_pending_upload_queue.emplace(std::move(chunk));
});
}
bool ClientWorld::is_receive_exit() { return m_receive_exit; }
int ClientWorld::chunk_size() const { return m_chunks.size(); }
void ClientWorld::request_exit() {
if (m_receive_exit) {
return;
@@ -512,51 +537,60 @@ void ClientWorld::update(float delta_time) {
}
m_pending_delete_vao.clear();
}
std::vector<ClientChunk> new_chunks;
std::vector<std::unique_ptr<ClientChunk>> new_chunks;
{
std::lock_guard lock(m_pending_upload_queue_mutex);
for (auto& c : m_pending_upload_queue) {
// Logger::info("{} {}", c.get_chunk_pos().x, c.get_chunk_pos().z);
new_chunks.emplace_back(std::move(c));
std::unique_ptr<ClientChunk> chunk;
while (m_pending_upload_queue.try_pop(chunk)) {
new_chunks.emplace_back(std::move(chunk));
}
m_pending_upload_queue.clear();
}
for (auto& c : new_chunks) {
c.upload_to_gpu();
c->upload_to_gpu();
}
{
std::lock_guard lock(m_chunks_mutex);
for (auto& c : new_chunks) {
m_chunks.emplace(c.get_chunk_pos(), std::move(c));
m_chunks.emplace(c->get_chunk_pos(), std::move(c));
}
m_render_snapshots.clear();
for (auto& [pos, chunk] : m_chunks) {
if (chunk.is_dirty()) {
auto chunk_pos_set = m_player.get_chunk_pos_set();
for (auto& pos : chunk_pos_set) {
std::shared_ptr<ClientChunk> chunk;
{
chunk_acc acc;
if (m_chunks.find(acc, pos)) {
chunk = acc->second;
}
}
if (!chunk) {
continue;
}
if (chunk->is_dirty()) {
// the curial fator influence
OptionalBlockVectorArray neighbor_block;
for (int i = 0; i < 4; i++) {
auto it = m_chunks.find(pos + CHUNK_DIR[i]);
if (it != m_chunks.end()) {
neighbor_block[i] = (it->second.get_chunk_blocks());
chunk_cacc cacc;
if (m_chunks.find(cacc, pos + CHUNK_DIR[i])) {
neighbor_block[i] = (cacc->second->get_chunk_blocks());
} else {
neighbor_block[i] = std::nullopt;
}
}
chunk.gen_vertex_data(neighbor_block);
chunk.upload_to_gpu();
chunk->gen_vertex_data(neighbor_block);
chunk->upload_to_gpu();
}
if (!chunk.is_dirty()) {
if (chunk.is_need_upload()) {
chunk.upload_to_gpu();
if (!chunk->is_dirty()) {
if (chunk->is_need_upload()) {
chunk->upload_to_gpu();
}
m_render_snapshots.push_back(
{chunk.get_normal_vao(), chunk.get_normal_vertices_sum(),
chunk.get_cross_vao(), chunk.get_cross_vertices_sum(),
chunk.get_normal_discard_vao(),
chunk.get_normal_discard_vertices_sum(),
chunk.get_normal_blend_vao(),
chunk.get_normal_blend_vertices_sum(),
chunk.get_water_vao(), chunk.get_water_vertices_sum(),
{chunk->get_normal_vao(), chunk->get_normal_vertices_sum(),
chunk->get_cross_vao(), chunk->get_cross_vertices_sum(),
chunk->get_normal_discard_vao(),
chunk->get_normal_discard_vertices_sum(),
chunk->get_normal_blend_vao(),
chunk->get_normal_blend_vertices_sum(),
chunk->get_water_vao(), chunk->get_water_vertices_sum(),
glm::vec3(static_cast<float>(pos.x * CHUNK_SIZE) +
static_cast<float>(CHUNK_SIZE / 2),
static_cast<float>(WORLD_SIZE_Y / 2),

View File

@@ -59,6 +59,7 @@ asio::awaitable<void> NetworkClient::read_loop() {
throw std::runtime_error("invalid packet length");
}
// maybe move, don't use it after switch!
std::vector<uint8_t> body_data(header.compressed_size);
if (header.compressed_size > 0) {
co_await asio::async_read(m_socket, asio::buffer(body_data),
@@ -80,12 +81,9 @@ asio::awaitable<void> NetworkClient::read_loop() {
}
} break;
case std::to_underlying(PacketEnum::CHUNK_DATA_RSP): {
ChunkDataRsp rsp;
// Logger::info("Client: Receive Chunk Data rsp, size {}mb",
// body_data.size() / 1024.0f / 1024);
if (decode_packet(rsp, body_data, header)) {
m_world.receive_chunk(std::move(rsp));
}
m_world.receive_chunk(std::move(body_data), header);
} break;
case std::to_underlying(PacketEnum::BLOCK_CHANGE_RSP): {
auto* rsp = Arena::Create<BlockChangeRsp>(&arena);

View File

@@ -1,5 +1,14 @@
#include "Cubed/app.hpp"
#ifdef _WIN32
extern "C" {
__declspec(dllexport) unsigned long NvOptimusEnablement = 1;
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif
int main(int argc, char** argv) {
static_assert(sizeof(int) == sizeof(int32_t));