Доработка пайплайн машины (требуется пересмотр технологии)

This commit is contained in:
2026-01-03 00:41:09 +06:00
parent f56b46f669
commit 776e9bfaca
31 changed files with 2684 additions and 601 deletions

View File

@@ -13,8 +13,8 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdata-sections -ffunction-sections -DGL
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") # -rdynamic set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") # -rdynamic
# gprof # gprof
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg")
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg")
# sanitizer # sanitizer
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")

View File

@@ -112,7 +112,6 @@ struct DefPortalInfo {
}; };
struct DefEntityInfo { struct DefEntityInfo {
}; };
struct DefFuncEntityInfo { struct DefFuncEntityInfo {
@@ -138,7 +137,10 @@ struct PortalInfo {
}; };
struct EntityInfo { struct EntityInfo {
DefEntityId DefId = 0;
WorldId_t WorldId = 0;
Pos::Object Pos = Pos::Object(0);
glm::quat Quat = glm::quat(1.f, 0.f, 0.f, 0.f);
}; };
struct FuncEntityInfo { struct FuncEntityInfo {
@@ -232,4 +234,4 @@ public:
virtual ~ISurfaceEventListener(); virtual ~ISurfaceEventListener();
}; };
} }

View File

@@ -606,10 +606,12 @@ void ServerSession::update(GlobalTime gTime, float dTime) {
std::vector<DefWorldId> profile_World_Lost; std::vector<DefWorldId> profile_World_Lost;
std::unordered_map<DefPortalId, void*> profile_Portal_AddOrChange; std::unordered_map<DefPortalId, void*> profile_Portal_AddOrChange;
std::vector<DefPortalId> profile_Portal_Lost; std::vector<DefPortalId> profile_Portal_Lost;
std::unordered_map<DefEntityId, void*> profile_Entity_AddOrChange; std::unordered_map<DefEntityId, DefEntityInfo> profile_Entity_AddOrChange;
std::vector<DefEntityId> profile_Entity_Lost; std::vector<DefEntityId> profile_Entity_Lost;
std::unordered_map<DefItemId, void*> profile_Item_AddOrChange; std::unordered_map<DefItemId, void*> profile_Item_AddOrChange;
std::vector<DefItemId> profile_Item_Lost; std::vector<DefItemId> profile_Item_Lost;
std::unordered_map<EntityId_t, EntityInfo> entity_AddOrChange;
std::vector<EntityId_t> entity_Lost;
{ {
for(TickData& data : ticks) { for(TickData& data : ticks) {
@@ -726,6 +728,25 @@ void ServerSession::update(GlobalTime gTime, float dTime) {
auto eraseIter = std::unique(profile_Item_Lost.begin(), profile_Item_Lost.end()); auto eraseIter = std::unique(profile_Item_Lost.begin(), profile_Item_Lost.end());
profile_Item_Lost.erase(eraseIter, profile_Item_Lost.end()); profile_Item_Lost.erase(eraseIter, profile_Item_Lost.end());
} }
{
for(auto& [id, info] : data.Entity_AddOrChange) {
auto iter = std::lower_bound(entity_Lost.begin(), entity_Lost.end(), id);
if(iter != entity_Lost.end() && *iter == id)
entity_Lost.erase(iter);
entity_AddOrChange[id] = info;
}
for(EntityId_t id : data.Entity_Lost) {
entity_AddOrChange.erase(id);
}
entity_Lost.insert(entity_Lost.end(), data.Entity_Lost.begin(), data.Entity_Lost.end());
std::sort(entity_Lost.begin(), entity_Lost.end());
auto eraseIter = std::unique(entity_Lost.begin(), entity_Lost.end());
entity_Lost.erase(eraseIter, entity_Lost.end());
}
} }
for(auto& [id, _] : profile_Voxel_AddOrChange) for(auto& [id, _] : profile_Voxel_AddOrChange)
@@ -829,6 +850,11 @@ void ServerSession::update(GlobalTime gTime, float dTime) {
auto& c = chunks_Changed[wId]; auto& c = chunks_Changed[wId];
for(auto& [pos, val] : list) { for(auto& [pos, val] : list) {
auto& sizes = VisibleChunkCompressed[wId][pos];
VisibleChunkCompressedBytes -= sizes.Voxels;
sizes.Voxels = val.size();
VisibleChunkCompressedBytes += sizes.Voxels;
caocvr[pos] = unCompressVoxels(val); caocvr[pos] = unCompressVoxels(val);
c.push_back(pos); c.push_back(pos);
} }
@@ -839,6 +865,11 @@ void ServerSession::update(GlobalTime gTime, float dTime) {
auto& c = chunks_Changed[wId]; auto& c = chunks_Changed[wId];
for(auto& [pos, val] : list) { for(auto& [pos, val] : list) {
auto& sizes = VisibleChunkCompressed[wId][pos];
VisibleChunkCompressedBytes -= sizes.Nodes;
sizes.Nodes = val.size();
VisibleChunkCompressedBytes += sizes.Nodes;
auto& chunkNodes = caocvr[pos]; auto& chunkNodes = caocvr[pos];
unCompressNodes(val, chunkNodes.data()); unCompressNodes(val, chunkNodes.data());
debugCheckGeneratedChunkNodes(wId, pos, chunkNodes); debugCheckGeneratedChunkNodes(wId, pos, chunkNodes);
@@ -985,20 +1016,41 @@ void ServerSession::update(GlobalTime gTime, float dTime) {
for(auto& [resId, def] : profile_Node_AddOrChange) { for(auto& [resId, def] : profile_Node_AddOrChange) {
Profiles.DefNode[resId] = def; Profiles.DefNode[resId] = def;
} }
for(auto& [resId, def] : profile_Entity_AddOrChange) {
Profiles.DefEntity[resId] = def;
}
} }
// Чанки // Чанки
{ {
for(auto& [wId, lost] : regions_Lost_Result) { for(auto& [wId, lost] : regions_Lost_Result) {
auto iterWorld = Content.Worlds.find(wId); auto iterWorld = Content.Worlds.find(wId);
if(iterWorld == Content.Worlds.end()) auto iterSizesWorld = VisibleChunkCompressed.find(wId);
if(iterWorld != Content.Worlds.end()) {
for(const Pos::GlobalRegion& rPos : lost) {
auto iterRegion = iterWorld->second.Regions.find(rPos);
if(iterRegion != iterWorld->second.Regions.end())
iterWorld->second.Regions.erase(iterRegion);
}
}
if(iterSizesWorld == VisibleChunkCompressed.end())
continue; continue;
for(const Pos::GlobalRegion& rPos : lost) { for(const Pos::GlobalRegion& rPos : lost) {
auto iterRegion = iterWorld->second.Regions.find(rPos); for(auto iter = iterSizesWorld->second.begin(); iter != iterSizesWorld->second.end(); ) {
if(iterRegion != iterWorld->second.Regions.end()) if(Pos::GlobalRegion(iter->first >> 2) == rPos) {
iterWorld->second.Regions.erase(iterRegion); VisibleChunkCompressedBytes -= iter->second.Voxels;
VisibleChunkCompressedBytes -= iter->second.Nodes;
iter = iterSizesWorld->second.erase(iter);
} else {
++iter;
}
}
} }
if(iterSizesWorld->second.empty())
VisibleChunkCompressed.erase(iterSizesWorld);
} }
for(auto& [wId, voxels] : chunks_AddOrChange_Voxel_Result) { for(auto& [wId, voxels] : chunks_AddOrChange_Voxel_Result) {
@@ -1019,6 +1071,43 @@ void ServerSession::update(GlobalTime gTime, float dTime) {
} }
// Сущности
{
for(auto& [entityId, info] : entity_AddOrChange) {
auto iter = Content.Entityes.find(entityId);
if(iter != Content.Entityes.end() && iter->second.WorldId != info.WorldId) {
auto iterWorld = Content.Worlds.find(iter->second.WorldId);
if(iterWorld != Content.Worlds.end()) {
auto &list = iterWorld->second.Entitys;
list.erase(std::remove(list.begin(), list.end(), entityId), list.end());
}
}
Content.Entityes[entityId] = info;
auto &list = Content.Worlds[info.WorldId].Entitys;
if(std::find(list.begin(), list.end(), entityId) == list.end())
list.push_back(entityId);
}
for(EntityId_t entityId : entity_Lost) {
auto iter = Content.Entityes.find(entityId);
if(iter != Content.Entityes.end()) {
auto iterWorld = Content.Worlds.find(iter->second.WorldId);
if(iterWorld != Content.Worlds.end()) {
auto &list = iterWorld->second.Entitys;
list.erase(std::remove(list.begin(), list.end(), entityId), list.end());
}
Content.Entityes.erase(iter);
} else {
for(auto& [wId, worldInfo] : Content.Worlds) {
auto &list = worldInfo.Entitys;
list.erase(std::remove(list.begin(), list.end(), entityId), list.end());
}
}
}
}
if(RS) if(RS)
RS->tickSync(result); RS->tickSync(result);
} }
@@ -1402,11 +1491,18 @@ coro<> ServerSession::rP_Definition(Net::AsyncSocket &sock) {
co_return; co_return;
case ToClient::L2Definition::Entity: case ToClient::L2Definition::Entity:
{
DefEntityId id = co_await sock.read<DefEntityId>();
DefEntityInfo def;
AsyncContext.ThisTickEntry.Profile_Entity_AddOrChange.emplace_back(id, def);
co_return; co_return;
}
case ToClient::L2Definition::FreeEntity: case ToClient::L2Definition::FreeEntity:
{
DefEntityId id = co_await sock.read<DefEntityId>();
AsyncContext.ThisTickEntry.Profile_Entity_Lost.push_back(id);
co_return; co_return;
}
default: default:
protocolError(); protocolError();
} }
@@ -1433,11 +1529,35 @@ coro<> ServerSession::rP_Content(Net::AsyncSocket &sock) {
co_return; co_return;
case ToClient::L2Content::Entity: case ToClient::L2Content::Entity:
{
EntityId_t id = co_await sock.read<EntityId_t>();
DefEntityId defId = co_await sock.read<DefEntityId>();
WorldId_t worldId = co_await sock.read<WorldId_t>();
Pos::Object pos;
pos.x = co_await sock.read<decltype(pos.x)>();
pos.y = co_await sock.read<decltype(pos.y)>();
pos.z = co_await sock.read<decltype(pos.z)>();
ToServer::PacketQuat q;
for(int iter = 0; iter < 5; iter++)
q.Data[iter] = co_await sock.read<uint8_t>();
EntityInfo info;
info.DefId = defId;
info.WorldId = worldId;
info.Pos = pos;
info.Quat = q.toQuat();
AsyncContext.ThisTickEntry.Entity_AddOrChange.emplace_back(id, info);
co_return; co_return;
}
case ToClient::L2Content::RemoveEntity: case ToClient::L2Content::RemoveEntity:
{
EntityId_t id = co_await sock.read<EntityId_t>();
AsyncContext.ThisTickEntry.Entity_Lost.push_back(id);
co_return; co_return;
}
case ToClient::L2Content::ChunkVoxels: case ToClient::L2Content::ChunkVoxels:
{ {
WorldId_t wcId = co_await sock.read<WorldId_t>(); WorldId_t wcId = co_await sock.read<WorldId_t>();

View File

@@ -42,6 +42,10 @@ public:
return Socket->isAlive() && IsConnected; return Socket->isAlive() && IsConnected;
} }
uint64_t getVisibleCompressedChunksBytes() const {
return VisibleChunkCompressedBytes;
}
// ISurfaceEventListener // ISurfaceEventListener
virtual void onResize(uint32_t width, uint32_t height) override; virtual void onResize(uint32_t width, uint32_t height) override;
@@ -99,7 +103,7 @@ private:
std::vector<DefWorldId> Profile_World_Lost; std::vector<DefWorldId> Profile_World_Lost;
std::vector<std::pair<DefPortalId, void*>> Profile_Portal_AddOrChange; std::vector<std::pair<DefPortalId, void*>> Profile_Portal_AddOrChange;
std::vector<DefPortalId> Profile_Portal_Lost; std::vector<DefPortalId> Profile_Portal_Lost;
std::vector<std::pair<DefEntityId, void*>> Profile_Entity_AddOrChange; std::vector<std::pair<DefEntityId, DefEntityInfo>> Profile_Entity_AddOrChange;
std::vector<DefEntityId> Profile_Entity_Lost; std::vector<DefEntityId> Profile_Entity_Lost;
std::vector<std::pair<DefItemId, void*>> Profile_Item_AddOrChange; std::vector<std::pair<DefItemId, void*>> Profile_Item_AddOrChange;
std::vector<DefItemId> Profile_Item_Lost; std::vector<DefItemId> Profile_Item_Lost;
@@ -110,6 +114,13 @@ private:
std::unordered_map<WorldId_t, std::unordered_map<Pos::GlobalChunk, std::u8string>> Chunks_AddOrChange_Voxel; std::unordered_map<WorldId_t, std::unordered_map<Pos::GlobalChunk, std::u8string>> Chunks_AddOrChange_Voxel;
std::unordered_map<WorldId_t, std::unordered_map<Pos::GlobalChunk, std::u8string>> Chunks_AddOrChange_Node; std::unordered_map<WorldId_t, std::unordered_map<Pos::GlobalChunk, std::u8string>> Chunks_AddOrChange_Node;
std::unordered_map<WorldId_t, std::vector<Pos::GlobalRegion>> Regions_Lost; std::unordered_map<WorldId_t, std::vector<Pos::GlobalRegion>> Regions_Lost;
std::vector<std::pair<EntityId_t, EntityInfo>> Entity_AddOrChange;
std::vector<EntityId_t> Entity_Lost;
};
struct ChunkCompressedSize {
uint32_t Voxels = 0;
uint32_t Nodes = 0;
}; };
struct AssetsBindsChange { struct AssetsBindsChange {
@@ -169,6 +180,9 @@ private:
GlobalTime LastSendPYR_POS; GlobalTime LastSendPYR_POS;
std::unordered_map<WorldId_t, std::unordered_map<Pos::GlobalChunk, ChunkCompressedSize>> VisibleChunkCompressed;
uint64_t VisibleChunkCompressedBytes = 0;
// Приём данных с сокета // Приём данных с сокета
coro<> run(AsyncUseControl::Lock); coro<> run(AsyncUseControl::Lock);
void protocolError(); void protocolError();

View File

@@ -14,6 +14,34 @@ PipelinedTextureAtlas::AtlasTextureId PipelinedTextureAtlas::getByPipeline(const
_AddictedTextures[texId].push_back(pipeline); _AddictedTextures[texId].push_back(pipeline);
} }
{
std::vector<TexturePipelineProgram::AnimSpec> animMeta =
TexturePipelineProgram::extractAnimationSpecs(pipeline._Pipeline.data(), pipeline._Pipeline.size());
if (!animMeta.empty()) {
AnimatedPipelineState entry;
entry.Specs.reserve(animMeta.size());
for (const auto& spec : animMeta) {
detail::AnimSpec16 outSpec{};
outSpec.TexId = spec.HasTexId ? spec.TexId : TextureAtlas::kOverflowId;
outSpec.FrameW = spec.FrameW;
outSpec.FrameH = spec.FrameH;
outSpec.FrameCount = spec.FrameCount;
outSpec.FpsQ = spec.FpsQ;
outSpec.Flags = spec.Flags;
entry.Specs.push_back(outSpec);
}
entry.LastFrames.resize(entry.Specs.size(), std::numeric_limits<uint32_t>::max());
entry.Smooth = false;
for (const auto& spec : entry.Specs) {
if (spec.Flags & detail::AnimSmooth) {
entry.Smooth = true;
break;
}
}
_AnimatedPipelines.emplace(pipeline, std::move(entry));
}
}
return atlasTexId; return atlasTexId;
} }
@@ -37,6 +65,7 @@ void PipelinedTextureAtlas::freeByPipeline(const HashedPipeline& pipeline) {
Super.removeTexture(iter->second); Super.removeTexture(iter->second);
_AtlasCpuTextures.erase(iter->second); _AtlasCpuTextures.erase(iter->second);
_PipeToTexId.erase(iter); _PipeToTexId.erase(iter);
_AnimatedPipelines.erase(pipeline);
} }
void PipelinedTextureAtlas::updateTexture(uint32_t texId, const StoredTexture& texture) { void PipelinedTextureAtlas::updateTexture(uint32_t texId, const StoredTexture& texture) {
@@ -90,7 +119,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli
} }
TexturePipelineProgram program; TexturePipelineProgram program;
program.fromWords(std::move(words)); program.fromBytes(std::move(words));
TexturePipelineProgram::OwnedTexture baked; TexturePipelineProgram::OwnedTexture baked;
auto provider = [this](uint32_t texId) -> std::optional<Texture> { auto provider = [this](uint32_t texId) -> std::optional<Texture> {
@@ -109,7 +138,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli
return tex; return tex;
}; };
if (!program.bake(provider, baked, nullptr)) { if (!program.bake(provider, baked, _AnimTimeSeconds, nullptr)) {
if (auto tex = tryCopyFirstDependencyTexture(pipeline)) { if (auto tex = tryCopyFirstDependencyTexture(pipeline)) {
return *tex; return *tex;
} }
@@ -135,6 +164,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli
void PipelinedTextureAtlas::flushNewPipelines() { void PipelinedTextureAtlas::flushNewPipelines() {
std::vector<uint32_t> changedTextures = std::move(_ChangedTextures); std::vector<uint32_t> changedTextures = std::move(_ChangedTextures);
_ChangedTextures.clear();
std::sort(changedTextures.begin(), changedTextures.end()); std::sort(changedTextures.begin(), changedTextures.end());
changedTextures.erase(std::unique(changedTextures.begin(), changedTextures.end()), changedTextures.end()); changedTextures.erase(std::unique(changedTextures.begin(), changedTextures.end()), changedTextures.end());
@@ -150,6 +180,7 @@ void PipelinedTextureAtlas::flushNewPipelines() {
} }
changedPipelineTextures.append_range(std::move(_ChangedPipelines)); changedPipelineTextures.append_range(std::move(_ChangedPipelines));
_ChangedPipelines.clear();
changedTextures.clear(); changedTextures.clear();
std::sort(changedPipelineTextures.begin(), changedPipelineTextures.end()); std::sort(changedPipelineTextures.begin(), changedPipelineTextures.end());
@@ -165,6 +196,18 @@ void PipelinedTextureAtlas::flushNewPipelines() {
auto& stored = _AtlasCpuTextures[atlasTexId]; auto& stored = _AtlasCpuTextures[atlasTexId];
stored = std::move(texture); stored = std::move(texture);
if (!stored._Pixels.empty()) { if (!stored._Pixels.empty()) {
// Смена порядка пикселей
for (uint32_t& pixel : stored._Pixels) {
union {
struct { uint8_t r, g, b, a; } color;
uint32_t data;
};
data = pixel;
std::swap(color.r, color.b);
pixel = data;
}
Super.setTextureData(atlasTexId, Super.setTextureData(atlasTexId,
stored._Widht, stored._Widht,
stored._Height, stored._Height,
@@ -182,6 +225,72 @@ void PipelinedTextureAtlas::notifyGpuFinished() {
Super.notifyGpuFinished(); Super.notifyGpuFinished();
} }
bool PipelinedTextureAtlas::updateAnimatedPipelines(double timeSeconds) {
_AnimTimeSeconds = timeSeconds;
if (_AnimatedPipelines.empty()) {
return false;
}
bool changed = false;
for (auto& [pipeline, entry] : _AnimatedPipelines) {
if (entry.Specs.empty()) {
continue;
}
if (entry.Smooth) {
_ChangedPipelines.push_back(pipeline);
changed = true;
continue;
}
if (entry.LastFrames.size() != entry.Specs.size())
entry.LastFrames.assign(entry.Specs.size(), std::numeric_limits<uint32_t>::max());
bool pipelineChanged = false;
for (size_t i = 0; i < entry.Specs.size(); ++i) {
const auto& spec = entry.Specs[i];
uint32_t fpsQ = spec.FpsQ ? spec.FpsQ : TexturePipelineProgram::DefaultAnimFpsQ;
double fps = double(fpsQ) / 256.0;
double frameTime = timeSeconds * fps;
if (frameTime < 0.0)
frameTime = 0.0;
uint32_t frameCount = spec.FrameCount;
// Авторасчёт количества кадров
if (frameCount == 0) {
auto iterTex = _ResToTexture.find(spec.TexId);
if (iterTex != _ResToTexture.end()) {
uint32_t fw = spec.FrameW ? spec.FrameW : iterTex->second._Widht;
uint32_t fh = spec.FrameH ? spec.FrameH : iterTex->second._Widht;
if (fw > 0 && fh > 0) {
if (spec.Flags & detail::AnimHorizontal)
frameCount = iterTex->second._Widht / fw;
else
frameCount = iterTex->second._Height / fh;
}
}
}
if (frameCount == 0)
frameCount = 1;
uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u;
if (entry.LastFrames[i] != frameIndex) {
entry.LastFrames[i] = frameIndex;
pipelineChanged = true;
}
}
if (pipelineChanged) {
_ChangedPipelines.push_back(pipeline);
changed = true;
}
}
return changed;
}
std::optional<StoredTexture> PipelinedTextureAtlas::tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const { std::optional<StoredTexture> PipelinedTextureAtlas::tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const {
auto deps = pipeline.getDependencedTextures(); auto deps = pipeline.getDependencedTextures();
if (!deps.empty()) { if (!deps.empty()) {

View File

@@ -6,6 +6,7 @@
#include <cassert> #include <cassert>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <limits>
#include <optional> #include <optional>
#include <unordered_map> #include <unordered_map>
#include <utility> #include <utility>
@@ -22,6 +23,7 @@ enum class Op16 : Word {
End = 0, End = 0,
Base_Tex = 1, Base_Tex = 1,
Base_Fill = 2, Base_Fill = 2,
Base_Anim = 3,
Resize = 10, Resize = 10,
Transform = 11, Transform = 11,
Opacity = 12, Opacity = 12,
@@ -33,6 +35,7 @@ enum class Op16 : Word {
Multiply = 18, Multiply = 18,
Screen = 19, Screen = 19,
Colorize = 20, Colorize = 20,
Anim = 21,
Overlay = 30, Overlay = 30,
Mask = 31, Mask = 31,
LowPart = 32, LowPart = 32,
@@ -43,13 +46,24 @@ enum class SrcKind16 : Word { TexId = 0, Sub = 1 };
struct SrcRef16 { struct SrcRef16 {
SrcKind16 kind{SrcKind16::TexId}; SrcKind16 kind{SrcKind16::TexId};
Word a = 0; uint32_t TexId = 0;
Word b = 0; uint32_t Off = 0;
uint32_t Len = 0;
}; };
inline uint32_t makeU32(Word lo, Word hi) { enum AnimFlags16 : Word {
return uint32_t(lo) | (uint32_t(hi) << 16); AnimSmooth = 1 << 0,
} AnimHorizontal = 1 << 1
};
struct AnimSpec16 {
uint32_t TexId = 0;
uint16_t FrameW = 0;
uint16_t FrameH = 0;
uint16_t FrameCount = 0;
uint16_t FpsQ = 0;
uint16_t Flags = 0;
};
inline void addUniqueDep(boost::container::small_vector<uint32_t, 8>& deps, uint32_t id) { inline void addUniqueDep(boost::container::small_vector<uint32_t, 8>& deps, uint32_t id) {
if (id == TextureAtlas::kOverflowId) { if (id == TextureAtlas::kOverflowId) {
@@ -60,16 +74,52 @@ inline void addUniqueDep(boost::container::small_vector<uint32_t, 8>& deps, uint
} }
} }
inline bool readSrc(const std::vector<Word>& words, size_t end, size_t& ip, SrcRef16& out) { inline bool read16(const std::vector<Word>& words, size_t end, size_t& ip, uint16_t& out) {
if (ip + 1 >= end) {
return false;
}
out = uint16_t(words[ip]) | (uint16_t(words[ip + 1]) << 8);
ip += 2;
return true;
}
inline bool read24(const std::vector<Word>& words, size_t end, size_t& ip, uint32_t& out) {
if (ip + 2 >= end) { if (ip + 2 >= end) {
return false; return false;
} }
out.kind = static_cast<SrcKind16>(words[ip++]); out = uint32_t(words[ip]) |
out.a = words[ip++]; (uint32_t(words[ip + 1]) << 8) |
out.b = words[ip++]; (uint32_t(words[ip + 2]) << 16);
ip += 3;
return true; return true;
} }
inline bool read32(const std::vector<Word>& words, size_t end, size_t& ip, uint32_t& out) {
if (ip + 3 >= end) {
return false;
}
out = uint32_t(words[ip]) |
(uint32_t(words[ip + 1]) << 8) |
(uint32_t(words[ip + 2]) << 16) |
(uint32_t(words[ip + 3]) << 24);
ip += 4;
return true;
}
inline bool readSrc(const std::vector<Word>& words, size_t end, size_t& ip, SrcRef16& out) {
if (ip >= end) {
return false;
}
out.kind = static_cast<SrcKind16>(words[ip++]);
if (out.kind == SrcKind16::TexId) {
return read24(words, end, ip, out.TexId);
}
if (out.kind == SrcKind16::Sub) {
return read24(words, end, ip, out.Off) && read24(words, end, ip, out.Len);
}
return false;
}
inline void extractPipelineDependencies(const std::vector<Word>& words, inline void extractPipelineDependencies(const std::vector<Word>& words,
size_t start, size_t start,
size_t end, size_t end,
@@ -88,12 +138,12 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
auto need = [&](size_t n) { return ip + n <= end; }; auto need = [&](size_t n) { return ip + n <= end; };
auto handleSrc = [&](const SrcRef16& src) { auto handleSrc = [&](const SrcRef16& src) {
if (src.kind == SrcKind16::TexId) { if (src.kind == SrcKind16::TexId) {
addUniqueDep(deps, makeU32(src.a, src.b)); addUniqueDep(deps, src.TexId);
return; return;
} }
if (src.kind == SrcKind16::Sub) { if (src.kind == SrcKind16::Sub) {
size_t subStart = static_cast<size_t>(src.a); size_t subStart = static_cast<size_t>(src.Off);
size_t subEnd = subStart + static_cast<size_t>(src.b); size_t subEnd = subStart + static_cast<size_t>(src.Len);
if (subStart < subEnd && subEnd <= words.size()) { if (subStart < subEnd && subEnd <= words.size()) {
extractPipelineDependencies(words, subStart, subEnd, deps, visited); extractPipelineDependencies(words, subStart, subEnd, deps, visited);
} }
@@ -108,37 +158,54 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
return; return;
case Op16::Base_Tex: { case Op16::Base_Tex: {
if (!need(3)) return;
SrcRef16 src{}; SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return; if (!readSrc(words, end, ip, src)) return;
handleSrc(src); handleSrc(src);
} break; } break;
case Op16::Base_Fill: case Op16::Base_Anim: {
if (!need(4)) return; SrcRef16 src{};
ip += 4; if (!readSrc(words, end, ip, src)) return;
break; handleSrc(src);
uint16_t tmp16 = 0;
uint8_t tmp8 = 0;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!need(1)) return;
tmp8 = words[ip++];
(void)tmp8;
} break;
case Op16::Base_Fill: {
uint16_t tmp16 = 0;
uint32_t tmp32 = 0;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read32(words, end, ip, tmp32)) return;
} break;
case Op16::Overlay: case Op16::Overlay:
case Op16::Mask: { case Op16::Mask: {
if (!need(3)) return;
SrcRef16 src{}; SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return; if (!readSrc(words, end, ip, src)) return;
handleSrc(src); handleSrc(src);
} break; } break;
case Op16::LowPart: { case Op16::LowPart: {
if (!need(1 + 3)) return; if (!need(1)) return;
ip += 1; // percent ip += 1; // percent
SrcRef16 src{}; SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return; if (!readSrc(words, end, ip, src)) return;
handleSrc(src); handleSrc(src);
} break; } break;
case Op16::Resize: case Op16::Resize: {
if (!need(2)) return; uint16_t tmp16 = 0;
ip += 2; if (!read16(words, end, ip, tmp16)) return;
break; if (!read16(words, end, ip, tmp16)) return;
} break;
case Op16::Transform: case Op16::Transform:
case Op16::Opacity: case Op16::Opacity:
@@ -151,8 +218,8 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
break; break;
case Op16::MakeAlpha: case Op16::MakeAlpha:
if (!need(2)) return; if (!need(3)) return;
ip += 2; ip += 3;
break; break;
case Op16::Invert: case Op16::Invert:
@@ -166,27 +233,42 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
break; break;
case Op16::Multiply: case Op16::Multiply:
case Op16::Screen: case Op16::Screen: {
if (!need(2)) return; uint32_t tmp32 = 0;
ip += 2; if (!read32(words, end, ip, tmp32)) return;
break; } break;
case Op16::Colorize: case Op16::Colorize: {
if (!need(3)) return; uint32_t tmp32 = 0;
ip += 3; if (!read32(words, end, ip, tmp32)) return;
break; if (!need(1)) return;
ip += 1;
} break;
case Op16::Anim: {
uint16_t tmp16 = 0;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!need(1)) return;
ip += 1;
} break;
case Op16::Combine: { case Op16::Combine: {
if (!need(3)) return; uint16_t w = 0, h = 0, n = 0;
ip += 2; // skip w,h if (!read16(words, end, ip, w)) return;
uint32_t n = words[ip++]; if (!read16(words, end, ip, h)) return;
if (!read16(words, end, ip, n)) return;
for (uint32_t i = 0; i < n; ++i) { for (uint32_t i = 0; i < n; ++i) {
if (!need(2 + 3)) return; uint16_t tmp16 = 0;
ip += 2; // x, y if (!read16(words, end, ip, tmp16)) return; // x
if (!read16(words, end, ip, tmp16)) return; // y
SrcRef16 src{}; SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return; if (!readSrc(words, end, ip, src)) return;
handleSrc(src); handleSrc(src);
} }
(void)w; (void)h;
} break; } break;
default: default:
@@ -227,8 +309,9 @@ struct Pipeline {
_Pipeline = { _Pipeline = {
static_cast<detail::Word>(detail::Op16::Base_Tex), static_cast<detail::Word>(detail::Op16::Base_Tex),
static_cast<detail::Word>(detail::SrcKind16::TexId), static_cast<detail::Word>(detail::SrcKind16::TexId),
static_cast<detail::Word>(texId & 0xFFFFu), static_cast<detail::Word>(texId & 0xFFu),
static_cast<detail::Word>((texId >> 16) & 0xFFFFu), static_cast<detail::Word>((texId >> 8) & 0xFFu),
static_cast<detail::Word>((texId >> 16) & 0xFFu),
static_cast<detail::Word>(detail::Op16::End) static_cast<detail::Word>(detail::Op16::End)
}; };
} }
@@ -253,9 +336,7 @@ struct HashedPipeline {
constexpr std::size_t prime = 1099511628211ull; constexpr std::size_t prime = 1099511628211ull;
for(detail::Word w : _Pipeline) { for(detail::Word w : _Pipeline) {
hash ^= static_cast<uint8_t>(w & 0xFF); hash ^= static_cast<uint8_t>(w);
hash *= prime;
hash ^= static_cast<uint8_t>((w >> 8) & 0xFF);
hash *= prime; hash *= prime;
} }
@@ -328,6 +409,13 @@ private:
std::vector<uint32_t> _ChangedTextures; std::vector<uint32_t> _ChangedTextures;
// Необходимые к созданию/обновлению пайплайны // Необходимые к созданию/обновлению пайплайны
std::vector<HashedPipeline> _ChangedPipelines; std::vector<HashedPipeline> _ChangedPipelines;
struct AnimatedPipelineState {
std::vector<detail::AnimSpec16> Specs;
std::vector<uint32_t> LastFrames;
bool Smooth = false;
};
std::unordered_map<HashedPipeline, AnimatedPipelineState, HashedPipelineKeyHash, HashedPipelineKeyEqual> _AnimatedPipelines;
double _AnimTimeSeconds = 0.0;
public: public:
PipelinedTextureAtlas(TextureAtlas&& tk); PipelinedTextureAtlas(TextureAtlas&& tk);
@@ -348,6 +436,26 @@ public:
return atlasLayers(); return atlasLayers();
} }
uint32_t maxLayers() const {
return Super.maxLayers();
}
uint32_t maxTextureId() const {
return Super.maxTextureId();
}
TextureAtlas::TextureId reservedOverflowId() const {
return Super.reservedOverflowId();
}
TextureAtlas::TextureId reservedLayerId(uint32_t layer) const {
return Super.reservedLayerId(layer);
}
void requestLayerCount(uint32_t layers) {
Super.requestLayerCount(layers);
}
// Должны всегда бронировать идентификатор, либо отдавать kOverflowId. При этом запись tex+pipeline остаётся // Должны всегда бронировать идентификатор, либо отдавать kOverflowId. При этом запись tex+pipeline остаётся
// Выдаёт стабильный идентификатор, привязанный к пайплайну // Выдаёт стабильный идентификатор, привязанный к пайплайну
AtlasTextureId getByPipeline(const HashedPipeline& pipeline); AtlasTextureId getByPipeline(const HashedPipeline& pipeline);
@@ -373,6 +481,8 @@ public:
void notifyGpuFinished(); void notifyGpuFinished();
bool updateAnimatedPipelines(double timeSeconds);
private: private:
std::optional<StoredTexture> tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const; std::optional<StoredTexture> tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const;

View File

@@ -29,11 +29,13 @@ TextureAtlas::TextureAtlas(VkDevice device,
EntriesCpu_.resize(Cfg_.MaxTextureId); EntriesCpu_.resize(Cfg_.MaxTextureId);
std::memset(EntriesCpu_.data(), 0, EntriesCpu_.size() * sizeof(Entry)); std::memset(EntriesCpu_.data(), 0, EntriesCpu_.size() * sizeof(Entry));
_initReservedEntries();
EntriesDirty_ = true; EntriesDirty_ = true;
Slots_.resize(Cfg_.MaxTextureId); Slots_.resize(Cfg_.MaxTextureId);
FreeIds_.reserve(Cfg_.MaxTextureId); FreeIds_.reserve(Cfg_.MaxTextureId);
PendingInQueue_.assign(Cfg_.MaxTextureId, false); PendingInQueue_.assign(Cfg_.MaxTextureId, false);
NextId_ = _allocatableStart();
if(Cfg_.ExternalSampler != VK_NULL_HANDLE) { if(Cfg_.ExternalSampler != VK_NULL_HANDLE) {
Sampler_ = Cfg_.ExternalSampler; Sampler_ = Cfg_.ExternalSampler;
@@ -70,13 +72,19 @@ TextureAtlas::TextureId TextureAtlas::registerTexture() {
_ensureAliveOrThrow(); _ensureAliveOrThrow();
TextureId id = kOverflowId; TextureId id = kOverflowId;
if(NextId_ < _allocatableStart()) {
NextId_ = _allocatableStart();
}
while(!FreeIds_.empty() && isReservedId(FreeIds_.back())) {
FreeIds_.pop_back();
}
if(!FreeIds_.empty()) { if(!FreeIds_.empty()) {
id = FreeIds_.back(); id = FreeIds_.back();
FreeIds_.pop_back(); FreeIds_.pop_back();
} else if(NextId_ < Cfg_.MaxTextureId) { } else if(NextId_ < _allocatableLimit()) {
id = NextId_++; id = NextId_++;
} else { } else {
return kOverflowId; return reservedOverflowId();
} }
Slot& s = Slots_[id]; Slot& s = Slots_[id];
@@ -96,7 +104,7 @@ void TextureAtlas::setTextureData(TextureId id,
const void* pixelsRGBA8, const void* pixelsRGBA8,
uint32_t rowPitchBytes) { uint32_t rowPitchBytes) {
_ensureAliveOrThrow(); _ensureAliveOrThrow();
if(id == kOverflowId) return; if(isInvalidId(id)) return;
_ensureRegisteredIdOrThrow(id); _ensureRegisteredIdOrThrow(id);
if(w == 0 || h == 0) { if(w == 0 || h == 0) {
@@ -151,7 +159,7 @@ void TextureAtlas::setTextureData(TextureId id,
void TextureAtlas::clearTextureData(TextureId id) { void TextureAtlas::clearTextureData(TextureId id) {
_ensureAliveOrThrow(); _ensureAliveOrThrow();
if(id == kOverflowId) return; if(isInvalidId(id)) return;
_ensureRegisteredIdOrThrow(id); _ensureRegisteredIdOrThrow(id);
Slot& s = Slots_[id]; Slot& s = Slots_[id];
@@ -172,7 +180,7 @@ void TextureAtlas::clearTextureData(TextureId id) {
void TextureAtlas::removeTexture(TextureId id) { void TextureAtlas::removeTexture(TextureId id) {
_ensureAliveOrThrow(); _ensureAliveOrThrow();
if(id == kOverflowId) return; if(isInvalidId(id)) return;
_ensureRegisteredIdOrThrow(id); _ensureRegisteredIdOrThrow(id);
Slot& s = Slots_[id]; Slot& s = Slots_[id];
@@ -217,7 +225,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe
while (!queue.empty()) { while (!queue.empty()) {
TextureId id = queue.front(); TextureId id = queue.front();
queue.pop_front(); queue.pop_front();
if(id == kOverflowId || id >= inQueue.size()) { if(isInvalidId(id) || id >= inQueue.size()) {
continue; continue;
} }
if(!inQueue[id]) { if(!inQueue[id]) {
@@ -253,7 +261,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe
bool outOfSpace = false; bool outOfSpace = false;
for(TextureId id : pendingNow) { for(TextureId id : pendingNow) {
if(id == kOverflowId) continue; if(isInvalidId(id)) continue;
if(id >= Slots_.size()) continue; if(id >= Slots_.size()) continue;
Slot& s = Slots_[id]; Slot& s = Slots_[id];
if(!s.InUse || !s.HasCpuData) continue; if(!s.InUse || !s.HasCpuData) continue;
@@ -310,7 +318,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe
}; };
for(TextureId id : pendingNow) { for(TextureId id : pendingNow) {
if(id == kOverflowId) continue; if(isInvalidId(id)) continue;
Slot& s = Slots_[id]; Slot& s = Slots_[id];
if(!s.InUse || !s.HasCpuData || !s.HasPlacement) continue; if(!s.InUse || !s.HasCpuData || !s.HasPlacement) continue;
if(!uploadTextureIntoAtlas(s, s.Place, Atlas_, false)) { if(!uploadTextureIntoAtlas(s, s.Place, Atlas_, false)) {

View File

@@ -10,7 +10,7 @@ TextureAtlas — как пользоваться (кратко)
2) Зарегистрируйте текстуру и получите стабильный ID: 2) Зарегистрируйте текстуру и получите стабильный ID:
TextureId id = atlas.registerTexture(); TextureId id = atlas.registerTexture();
if(id == TextureAtlas::kOverflowId) { ... } // нет свободных ID if(id == TextureAtlas::kOverflowId || id == atlas.reservedOverflowId()) { ... } // нет свободных ID
3) Задайте данные (RGBA8), можно много раз — ID не меняется: 3) Задайте данные (RGBA8), можно много раз — ID не меняется:
atlas.setTextureData(id, w, h, pixels, rowPitchBytes); atlas.setTextureData(id, w, h, pixels, rowPitchBytes);
@@ -32,7 +32,11 @@ TextureAtlas — как пользоваться (кратко)
atlas.removeTexture(id); // освободить ID (после этого использовать нельзя) atlas.removeTexture(id); // освободить ID (после этого использовать нельзя)
Примечания: Примечания:
- Вызовы API с kOverflowId игнорируются (no-op). - Вызовы API с kOverflowId и зарезервированными ID игнорируются (no-op).
- ID из начала диапазона зарезервированы под служебные нужды:
reservedOverflowId() == 0,
reservedLayerId(0) == 1 (первый слой),
reservedLayerId(1) == 2 (второй слой) и т.д.
- Ошибки ресурсов (нет места/стейджинга/oom) НЕ бросают исключения — дают события. - Ошибки ресурсов (нет места/стейджинга/oom) НЕ бросают исключения — дают события.
- Исключения только за неверный ввод/неверное использование (см. ТЗ). - Исключения только за неверный ввод/неверное использование (см. ТЗ).
- Класс не thread-safe: синхронизацию обеспечивает пользователь. - Класс не thread-safe: синхронизацию обеспечивает пользователь.
@@ -192,11 +196,60 @@ public:
/// Текущее число слоёв атласа. /// Текущее число слоёв атласа.
uint32_t atlasLayers() const { return Atlas_.Layers; } uint32_t atlasLayers() const { return Atlas_.Layers; }
uint32_t maxLayers() const { return Cfg_.MaxLayers; }
uint32_t maxTextureId() const { return Cfg_.MaxTextureId; }
TextureId reservedOverflowId() const { return 0; }
TextureId reservedLayerId(uint32_t layer) const { return 1u + layer; }
bool isReservedId(TextureId id) const {
if(id >= Cfg_.MaxTextureId) return false;
return id < _reservedCount();
}
bool isReservedLayerId(TextureId id) const {
if(id >= Cfg_.MaxTextureId) return false;
return id != reservedOverflowId() && id < _reservedCount();
}
bool isInvalidId(TextureId id) const {
return id == kOverflowId || isReservedId(id);
}
void requestLayerCount(uint32_t layers) {
_ensureAliveOrThrow();
_scheduleLayerGrow(layers);
}
/// Общий staging-буфер (может быть задан извне). /// Общий staging-буфер (может быть задан извне).
std::shared_ptr<SharedStagingBuffer> getStagingBuffer() const { return Staging_; } std::shared_ptr<SharedStagingBuffer> getStagingBuffer() const { return Staging_; }
private: private:
void _moveFrom(TextureAtlas&& other) noexcept; void _moveFrom(TextureAtlas&& other) noexcept;
uint32_t _reservedCount() const { return Cfg_.MaxLayers + 1; }
uint32_t _reservedStart() const { return 0; }
uint32_t _allocatableStart() const { return _reservedCount(); }
uint32_t _allocatableLimit() const { return Cfg_.MaxTextureId; }
void _initReservedEntries() {
if(Cfg_.MaxTextureId <= _reservedCount()) {
return;
}
_setEntryInvalid(reservedOverflowId(), /*diagPending*/false, /*diagTooLarge*/false);
for(uint32_t layer = 0; layer < Cfg_.MaxLayers; ++layer) {
TextureId id = reservedLayerId(layer);
Entry& e = EntriesCpu_[id];
e.UVMinMax[0] = 0.0f;
e.UVMinMax[1] = 0.0f;
e.UVMinMax[2] = 1.0f;
e.UVMinMax[3] = 1.0f;
e.Layer = layer;
e.Flags = ENTRY_VALID;
}
EntriesDirty_ = true;
}
// ============================= Ошибки/валидация ============================= // ============================= Ошибки/валидация =============================
struct InputError : std::runtime_error { struct InputError : std::runtime_error {
@@ -225,6 +278,9 @@ private:
if(Cfg_.MaxTextureId == 0) { if(Cfg_.MaxTextureId == 0) {
throw _inputError("Config.MaxTextureId must be > 0"); throw _inputError("Config.MaxTextureId must be > 0");
} }
if(Cfg_.MaxTextureId <= (Cfg_.MaxLayers + 1)) {
throw _inputError("Config.MaxTextureId must be > MaxLayers + 1 (reserved ids)");
}
if(Cfg_.MaxTextureSize != 2048) { if(Cfg_.MaxTextureSize != 2048) {
/// TODO: /// TODO:
@@ -246,7 +302,7 @@ private:
} }
void _ensureRegisteredIdOrThrow(TextureId id) const { void _ensureRegisteredIdOrThrow(TextureId id) const {
if(id >= Cfg_.MaxTextureId) { if(id >= Cfg_.MaxTextureId || isReservedId(id)) {
throw _inputError("TextureId out of range"); throw _inputError("TextureId out of range");
} }
if(!Slots_[id].InUse || Slots_[id].StateValue == State::REMOVED) { if(!Slots_[id].InUse || Slots_[id].StateValue == State::REMOVED) {

File diff suppressed because it is too large Load Diff

View File

@@ -311,7 +311,7 @@ void Vulkan::run()
} }
if(Game.RSession) { if(Game.RSession) {
Game.RSession->beforeDraw(); Game.RSession->beforeDraw(double(gTime));
} }
{ {
@@ -624,12 +624,6 @@ void Vulkan::run()
err = vkQueuePresentKHR(*lockQueue, &present); err = vkQueuePresentKHR(*lockQueue, &present);
} }
{
auto lockQueue = Graphics.DeviceQueueGraphic.lock();
vkDeviceWaitIdle(Graphics.Device);
lockQueue.unlock();
}
if (err == VK_ERROR_OUT_OF_DATE_KHR) if (err == VK_ERROR_OUT_OF_DATE_KHR)
{ {
freeSwapchains(); freeSwapchains();
@@ -651,12 +645,6 @@ void Vulkan::run()
Screen.State = DrawState::End; Screen.State = DrawState::End;
} }
{
auto lockQueue = Graphics.DeviceQueueGraphic.lock();
vkDeviceWaitIdle(Graphics.Device);
lockQueue.unlock();
}
for(int iter = 0; iter < 4; iter++) { for(int iter = 0; iter < 4; iter++) {
vkDestroySemaphore(Graphics.Device, SemaphoreImageAcquired[iter], nullptr); vkDestroySemaphore(Graphics.Device, SemaphoreImageAcquired[iter], nullptr);
vkDestroySemaphore(Graphics.Device, SemaphoreDrawComplete[iter], nullptr); vkDestroySemaphore(Graphics.Device, SemaphoreDrawComplete[iter], nullptr);
@@ -688,8 +676,6 @@ uint32_t Vulkan::memoryTypeFromProperties(uint32_t bitsOfAcceptableTypes, VkFlag
void Vulkan::freeSwapchains() void Vulkan::freeSwapchains()
{ {
//vkDeviceWaitIdle(Screen.Device);
if(Graphics.Instance && Graphics.Device) if(Graphics.Instance && Graphics.Device)
{ {
std::vector<VkImageView> oldViews; std::vector<VkImageView> oldViews;
@@ -2302,6 +2288,9 @@ void Vulkan::gui_ConnectedToServer() {
(int) Game.RSession->PlayerPos.x >> 6, (int) Game.RSession->PlayerPos.y >> 6, (int) Game.RSession->PlayerPos.z >> 6 (int) Game.RSession->PlayerPos.x >> 6, (int) Game.RSession->PlayerPos.y >> 6, (int) Game.RSession->PlayerPos.z >> 6
); );
double chunksKb = double(Game.Session->getVisibleCompressedChunksBytes()) / 1024.0;
ImGui::Text("chunks compressed: %.1f KB", chunksKb);
if(ImGui::Button("Delimeter")) if(ImGui::Button("Delimeter"))
LOG.debug(); LOG.debug();

View File

@@ -323,7 +323,7 @@ void ChunkMeshGenerator::run(uint8_t id) {
}; };
std::unordered_map<uint32_t, ModelCacheEntry> modelCache; std::unordered_map<uint32_t, ModelCacheEntry> modelCache;
std::unordered_map<AssetsTexture, uint16_t> baseTextureCache; std::unordered_map<AssetsTexture, uint32_t> baseTextureCache;
std::vector<NodeStateInfo> metaStatesInfo; std::vector<NodeStateInfo> metaStatesInfo;
{ {
@@ -451,7 +451,7 @@ void ChunkMeshGenerator::run(uint8_t id) {
if(iterTex != baseTextureCache.end()) { if(iterTex != baseTextureCache.end()) {
v.Tex = iterTex->second; v.Tex = iterTex->second;
} else { } else {
uint16_t resolvedTex = NSP->getTextureId(node->TexId); uint32_t resolvedTex = NSP->getTextureId(node->TexId);
v.Tex = resolvedTex; v.Tex = resolvedTex;
baseTextureCache.emplace(node->TexId, resolvedTex); baseTextureCache.emplace(node->TexId, resolvedTex);
} }
@@ -1670,7 +1670,7 @@ void VulkanRenderSession::tickSync(const TickSyncData& data) {
} }
if(TP) { if(TP) {
std::vector<std::tuple<AssetsTexture, Resource>> textureResources; std::vector<TextureProvider::TextureUpdate> textureResources;
std::vector<AssetsTexture> textureLost; std::vector<AssetsTexture> textureLost;
if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Texture); iter != data.Assets_ChangeOrAdd.end()) { if(auto iter = data.Assets_ChangeOrAdd.find(EnumAssets::Texture); iter != data.Assets_ChangeOrAdd.end()) {
@@ -1680,7 +1680,12 @@ void VulkanRenderSession::tickSync(const TickSyncData& data) {
if(entryIter == list.end()) if(entryIter == list.end())
continue; continue;
textureResources.emplace_back(id, entryIter->second.Res); textureResources.push_back({
.Id = id,
.Res = entryIter->second.Res,
.Domain = entryIter->second.Domain,
.Key = entryIter->second.Key
});
} }
} }
@@ -1739,9 +1744,9 @@ void VulkanRenderSession::setCameraPos(WorldId_t worldId, Pos::Object pos, glm::
PlayerPos /= float(Pos::Object_t::BS); PlayerPos /= float(Pos::Object_t::BS);
} }
void VulkanRenderSession::beforeDraw() { void VulkanRenderSession::beforeDraw(double timeSeconds) {
if(TP) if(TP)
TP->update(); TP->update(timeSeconds);
LightDummy.atlasUpdateDynamicData(); LightDummy.atlasUpdateDynamicData();
CP.flushUploadsAndBarriers(VkInst->Graphics.CommandBufferRender); CP.flushUploadsAndBarriers(VkInst->Graphics.CommandBufferRender);
} }
@@ -1901,6 +1906,7 @@ void VulkanRenderSession::drawWorld(GlobalTime gTime, float dTime, VkCommandBuff
vkCmdDraw(drawCmd, 6*3*2, 1, 0, 0); vkCmdDraw(drawCmd, 6*3*2, 1, 0, 0);
{ {
PCO.Model = glm::mat4(1.0f);
Pos::GlobalChunk x64offset = X64Offset >> Pos::Object_t::BS_Bit >> 4; Pos::GlobalChunk x64offset = X64Offset >> Pos::Object_t::BS_Bit >> 4;
Pos::GlobalRegion x64offset_region = x64offset >> 2; Pos::GlobalRegion x64offset_region = x64offset >> 2;
@@ -1940,9 +1946,160 @@ void VulkanRenderSession::drawWorld(GlobalTime gTime, float dTime, VkCommandBuff
PCO.Model = orig; PCO.Model = orig;
} }
vkCmdBindPipeline(drawCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, NodeStaticTransparentPipeline);
vkCmdPushConstants(drawCmd, MainAtlas_LightMap_PipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_GEOMETRY_BIT, 0, sizeof(WorldPCO), &PCO);
vkCmdBindDescriptorSets(drawCmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
MainAtlas_LightMap_PipelineLayout, 0, 2,
(const VkDescriptorSet[]) {TP ? TP->getDescriptorSet() : VK_NULL_HANDLE, VoxelLightMapDescriptor}, 0, nullptr);
ensureAtlasLayerPreview();
if(AtlasLayersPreview) {
glm::mat4 previewModel = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 64.0f, 0.0f)-glm::vec3(X64Offset >> Pos::Object_t::BS_Bit));
vkCmdPushConstants(drawCmd, MainAtlas_LightMap_PipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_GEOMETRY_BIT, offsetof(WorldPCO, Model), sizeof(WorldPCO::Model), &previewModel);
VkBuffer previewBuffer = *AtlasLayersPreview;
VkDeviceSize previewOffset = 0;
vkCmdBindVertexBuffers(drawCmd, 0, 1, &previewBuffer, &previewOffset);
vkCmdDraw(drawCmd, AtlasLayersPreviewCount * 6, 1, 0, 0);
}
if(false) {
ensureEntityTexture();
if(!ServerSession->Content.Entityes.empty()) {
VkBuffer entityBuffer = TestQuad;
VkDeviceSize entityOffset = 0;
vkCmdBindVertexBuffers(drawCmd, 0, 1, &entityBuffer, &entityOffset);
glm::mat4 orig = PCO.Model;
for(const auto& pair : ServerSession->Content.Entityes) {
const auto& info = pair.second;
if(info.WorldId != WorldId)
continue;
glm::vec3 entityPos = Pos::Object_t::asFloatVec(info.Pos - X64Offset);
entityPos.y -= 1.6f; // Camera position arrives as eye height.
glm::mat4 model = glm::translate(glm::mat4(1.0f), entityPos);
model = model * glm::mat4(info.Quat);
model = glm::scale(model, glm::vec3(0.6f, 1.8f, 0.6f));
PCO.Model = model;
vkCmdPushConstants(drawCmd, MainAtlas_LightMap_PipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_GEOMETRY_BIT, offsetof(WorldPCO, Model), sizeof(WorldPCO::Model), &PCO.Model);
vkCmdDraw(drawCmd, 6*3*2, 1, 0, 0);
}
PCO.Model = orig;
}
}
CP.pushFrame(); CP.pushFrame();
} }
void VulkanRenderSession::updateTestQuadTexture(uint32_t texId) {
if(EntityTextureReady && EntityTextureId == texId)
return;
auto *array = reinterpret_cast<NodeVertexStatic*>(TestQuad.mapMemory());
const size_t vertexCount = TestQuad.getSize() / sizeof(NodeVertexStatic);
for(size_t iter = 0; iter < vertexCount; ++iter)
array[iter].Tex = texId;
TestQuad.unMapMemory();
EntityTextureId = texId;
EntityTextureReady = true;
}
void VulkanRenderSession::ensureEntityTexture() {
if(EntityTextureReady || !TP || !NSP)
return;
auto iter = ServerSession->Assets.find(EnumAssets::Texture);
if(iter == ServerSession->Assets.end() || iter->second.empty())
return;
const AssetEntry* picked = nullptr;
for(const auto& [id, entry] : iter->second) {
if(entry.Key == "default.png") {
picked = &entry;
break;
}
}
if(!picked) {
for(const auto& [id, entry] : iter->second) {
if(entry.Key == "grass.png") {
picked = &entry;
break;
}
}
}
if(!picked)
picked = &iter->second.begin()->second;
updateTestQuadTexture(NSP->getTextureId(picked->Id));
}
void VulkanRenderSession::ensureAtlasLayerPreview() {
if(!TP)
return;
const uint32_t maxLayers = TP->getAtlasMaxLayers();
if(maxLayers == 0)
return;
if(AtlasLayersPreview && AtlasLayersPreviewCount == maxLayers)
return;
TP->requestAtlasLayerCount(maxLayers);
const uint32_t vertsPerQuad = 6;
const uint32_t totalVerts = maxLayers * vertsPerQuad;
std::vector<NodeVertexStatic> verts(totalVerts);
const uint32_t columns = 4;
const uint32_t base = 224;
const uint32_t step = 320;
const uint32_t size = 256;
const uint32_t z = base;
auto makeVert = [&](uint32_t fx, uint32_t fy, uint32_t tex, uint32_t tu, uint32_t tv) -> NodeVertexStatic {
NodeVertexStatic v{};
v.FX = fx;
v.FY = fy;
v.FZ = z;
v.LS = 0;
v.Tex = tex;
v.TU = tu;
v.TV = tv;
return v;
};
for(uint32_t layer = 0; layer < maxLayers; ++layer) {
const uint32_t col = layer % columns;
const uint32_t row = layer / columns;
const uint32_t x0 = base + col * step;
const uint32_t y0 = base + row * step;
const uint32_t x1 = x0 + size;
const uint32_t y1 = y0 + size;
const uint32_t tex = TP->getAtlasLayerId(layer);
const size_t start = static_cast<size_t>(layer) * vertsPerQuad;
verts[start + 0] = makeVert(x0, y0, tex, 0, 0);
verts[start + 1] = makeVert(x0, y1, tex, 0, 65535);
verts[start + 2] = makeVert(x1, y1, tex, 65535, 65535);
verts[start + 3] = makeVert(x0, y0, tex, 0, 0);
verts[start + 4] = makeVert(x1, y1, tex, 65535, 65535);
verts[start + 5] = makeVert(x1, y0, tex, 65535, 0);
}
AtlasLayersPreview.emplace(VkInst, verts.size() * sizeof(NodeVertexStatic));
std::memcpy(AtlasLayersPreview->mapMemory(), verts.data(), verts.size() * sizeof(NodeVertexStatic));
AtlasLayersPreview->unMapMemory();
AtlasLayersPreviewCount = maxLayers;
}
void VulkanRenderSession::pushStage(EnumRenderStage stage) { void VulkanRenderSession::pushStage(EnumRenderStage stage) {
} }

View File

@@ -418,6 +418,13 @@ private:
*/ */
class TextureProvider { class TextureProvider {
public: public:
struct TextureUpdate {
AssetsTexture Id = 0;
Resource Res;
std::string Domain;
std::string Key;
};
TextureProvider(Vulkan* inst, VkDescriptorPool descPool) TextureProvider(Vulkan* inst, VkDescriptorPool descPool)
: Inst(inst), DescPool(descPool) : Inst(inst), DescPool(descPool)
{ {
@@ -508,32 +515,58 @@ public:
return Descriptor; return Descriptor;
} }
uint16_t getTextureId(const TexturePipeline& pipe) { uint32_t getTextureId(const TexturePipeline& pipe) {
std::lock_guard lock(Mutex); std::lock_guard lock(Mutex);
auto iter = PipelineToAtlas.find(pipe); bool animated = isAnimatedPipeline(pipe);
if(iter != PipelineToAtlas.end()) if(!animated) {
return iter->second; auto iter = PipelineToAtlas.find(pipe);
if(iter != PipelineToAtlas.end())
return iter->second;
}
::HashedPipeline hashed = makeHashedPipeline(pipe); ::HashedPipeline hashed = makeHashedPipeline(pipe);
uint32_t atlasId = Atlas->getByPipeline(hashed); uint32_t atlasId = Atlas->getByPipeline(hashed);
uint16_t result = 0; uint32_t result = atlasId;
if(atlasId <= std::numeric_limits<uint16_t>::max()) if(Atlas && result >= Atlas->maxTextureId()) {
result = static_cast<uint16_t>(atlasId);
else
LOG.warn() << "Atlas texture id overflow: " << atlasId; LOG.warn() << "Atlas texture id overflow: " << atlasId;
result = Atlas->reservedOverflowId();
}
PipelineToAtlas.emplace(pipe, result); if(!animated)
PipelineToAtlas.emplace(pipe, result);
NeedsUpload = true; NeedsUpload = true;
return result; return result;
} }
uint32_t getAtlasMaxLayers() const {
std::lock_guard lock(Mutex);
return Atlas ? Atlas->maxLayers() : 0u;
}
uint32_t getAtlasLayerId(uint32_t layer) const {
std::lock_guard lock(Mutex);
if(!Atlas)
return TextureAtlas::kOverflowId;
if(layer >= Atlas->maxLayers())
return Atlas->reservedOverflowId();
return Atlas->reservedLayerId(layer);
}
void requestAtlasLayerCount(uint32_t layers) {
std::lock_guard lock(Mutex);
if(Atlas)
Atlas->requestLayerCount(layers);
}
// Применяет изменения, возвращая все затронутые модели // Применяет изменения, возвращая все затронутые модели
std::vector<AssetsTexture> onTexturesChanges(std::vector<std::tuple<AssetsTexture, Resource>> newOrChanged, std::vector<AssetsTexture> lost) { std::vector<AssetsTexture> onTexturesChanges(std::vector<TextureUpdate> newOrChanged, std::vector<AssetsTexture> lost) {
std::lock_guard lock(Mutex); std::lock_guard lock(Mutex);
std::vector<AssetsTexture> result; std::vector<AssetsTexture> result;
for(const auto& [key, res] : newOrChanged) { for(const auto& update : newOrChanged) {
const AssetsTexture key = update.Id;
const Resource& res = update.Res;
result.push_back(key); result.push_back(key);
iResource sres((const uint8_t*) res.data(), res.size()); iResource sres((const uint8_t*) res.data(), res.size());
@@ -563,12 +596,19 @@ public:
std::move(pixels) std::move(pixels)
)); ));
if(auto anim = getDefaultAnimation(update.Key, width, height)) {
AnimatedSources[key] = *anim;
} else {
AnimatedSources.erase(key);
}
NeedsUpload = true; NeedsUpload = true;
} }
for(AssetsTexture key : lost) { for(AssetsTexture key : lost) {
result.push_back(key); result.push_back(key);
Atlas->freeTexture(key); Atlas->freeTexture(key);
AnimatedSources.erase(key);
NeedsUpload = true; NeedsUpload = true;
} }
@@ -579,9 +619,15 @@ public:
return result; return result;
} }
void update() { void update(double timeSeconds) {
std::lock_guard lock(Mutex); std::lock_guard lock(Mutex);
if(!NeedsUpload || !Atlas) if(!Atlas)
return;
if(Atlas->updateAnimatedPipelines(timeSeconds))
NeedsUpload = true;
if(!NeedsUpload)
return; return;
Atlas->flushNewPipelines(); Atlas->flushNewPipelines();
@@ -638,24 +684,94 @@ public:
} }
private: private:
struct AnimatedSource {
uint16_t FrameW = 0;
uint16_t FrameH = 0;
uint16_t FrameCount = 0;
uint16_t FpsQ = 0;
uint16_t Flags = 0;
};
static std::optional<AnimatedSource> getDefaultAnimation(std::string_view key, uint32_t width, uint32_t height) {
if(auto slash = key.find_last_of('/'); slash != std::string_view::npos)
key = key.substr(slash + 1);
if(key == "fire_0.png") {
AnimatedSource anim;
anim.FrameW = static_cast<uint16_t>(width);
anim.FrameH = static_cast<uint16_t>(width);
anim.FrameCount = static_cast<uint16_t>(width ? height / width : 0);
anim.FpsQ = static_cast<uint16_t>(12 * 256);
anim.Flags = 0;
return anim;
}
if(key == "lava_still.png") {
AnimatedSource anim;
anim.FrameW = static_cast<uint16_t>(width);
anim.FrameH = static_cast<uint16_t>(width);
anim.FrameCount = static_cast<uint16_t>(width ? height / width : 0);
anim.FpsQ = static_cast<uint16_t>(8 * 256);
anim.Flags = 0;
return anim;
}
if(key == "water_still.png") {
AnimatedSource anim;
anim.FrameW = static_cast<uint16_t>(width);
anim.FrameH = static_cast<uint16_t>(width);
anim.FrameCount = static_cast<uint16_t>(width ? height / width : 0);
anim.FpsQ = static_cast<uint16_t>(8 * 256);
anim.Flags = TexturePipelineProgram::AnimSmooth;
return anim;
}
return std::nullopt;
}
bool isAnimatedPipeline(const TexturePipeline& pipe) const {
if(!pipe.Pipeline.empty())
return false;
if(pipe.BinTextures.size() != 1)
return false;
return AnimatedSources.contains(pipe.BinTextures.front());
}
::HashedPipeline makeHashedPipeline(const TexturePipeline& pipe) const { ::HashedPipeline makeHashedPipeline(const TexturePipeline& pipe) const {
::Pipeline pipeline; ::Pipeline pipeline;
if(!pipe.Pipeline.empty() && (pipe.Pipeline.size() % 2u) == 0u) { if(!pipe.Pipeline.empty()) {
std::vector<TexturePipelineProgram::Word> words; const auto* bytes = reinterpret_cast<const ::detail::Word*>(pipe.Pipeline.data());
words.reserve(pipe.Pipeline.size() / 2u); pipeline._Pipeline.assign(bytes, bytes + pipe.Pipeline.size());
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(pipe.Pipeline.data());
for(size_t i = 0; i < pipe.Pipeline.size(); i += 2) {
uint16_t lo = bytes[i];
uint16_t hi = bytes[i + 1];
words.push_back(static_cast<TexturePipelineProgram::Word>(lo | (hi << 8)));
}
pipeline._Pipeline.assign(words.begin(), words.end());
} }
if(pipeline._Pipeline.empty()) { if(pipeline._Pipeline.empty()) {
if(!pipe.BinTextures.empty()) if(!pipe.BinTextures.empty()) {
pipeline = ::Pipeline(pipe.BinTextures.front()); AssetsTexture texId = pipe.BinTextures.front();
auto animIter = AnimatedSources.find(texId);
if(animIter != AnimatedSources.end()) {
const auto& anim = animIter->second;
pipeline._Pipeline.clear();
pipeline._Pipeline.reserve(1 + 1 + 3 + 2 + 2 + 2 + 2 + 1 + 1);
auto emit16 = [&](uint16_t v) {
pipeline._Pipeline.push_back(static_cast<::detail::Word>(v & 0xFFu));
pipeline._Pipeline.push_back(static_cast<::detail::Word>((v >> 8) & 0xFFu));
};
pipeline._Pipeline.push_back(static_cast<::detail::Word>(::detail::Op16::Base_Anim));
pipeline._Pipeline.push_back(static_cast<::detail::Word>(::detail::SrcKind16::TexId));
pipeline._Pipeline.push_back(static_cast<::detail::Word>(texId & 0xFFu));
pipeline._Pipeline.push_back(static_cast<::detail::Word>((texId >> 8) & 0xFFu));
pipeline._Pipeline.push_back(static_cast<::detail::Word>((texId >> 16) & 0xFFu));
emit16(anim.FrameW);
emit16(anim.FrameH);
emit16(anim.FrameCount);
emit16(anim.FpsQ);
pipeline._Pipeline.push_back(static_cast<::detail::Word>(anim.Flags & 0xFFu));
pipeline._Pipeline.push_back(static_cast<::detail::Word>(::detail::Op16::End));
} else {
pipeline = ::Pipeline(texId);
}
}
} }
return ::HashedPipeline(pipeline); return ::HashedPipeline(pipeline);
@@ -690,7 +806,8 @@ private:
std::shared_ptr<SharedStagingBuffer> AtlasStaging; std::shared_ptr<SharedStagingBuffer> AtlasStaging;
std::unique_ptr<PipelinedTextureAtlas> Atlas; std::unique_ptr<PipelinedTextureAtlas> Atlas;
std::unordered_map<TexturePipeline, uint16_t> PipelineToAtlas; std::unordered_map<TexturePipeline, uint32_t> PipelineToAtlas;
std::unordered_map<AssetsTexture, AnimatedSource> AnimatedSources;
bool NeedsUpload = false; bool NeedsUpload = false;
Logger LOG = "Client>TextureProvider"; Logger LOG = "Client>TextureProvider";
@@ -811,7 +928,7 @@ public:
} }
std::vector<std::vector<std::pair<float, std::unordered_map<EnumFace, std::vector<NodeVertexStatic>>>>> result; std::vector<std::vector<std::pair<float, std::unordered_map<EnumFace, std::vector<NodeVertexStatic>>>>> result;
std::unordered_map<TexturePipeline, uint16_t> pipelineResolveCache; std::unordered_map<TexturePipeline, uint32_t> pipelineResolveCache;
auto appendModel = [&](AssetsModel modelId, const std::vector<Transformation>& transforms, std::unordered_map<EnumFace, std::vector<NodeVertexStatic>>& out) { auto appendModel = [&](AssetsModel modelId, const std::vector<Transformation>& transforms, std::unordered_map<EnumFace, std::vector<NodeVertexStatic>>& out) {
ModelProvider::Model model = MP.getModel(modelId); ModelProvider::Model model = MP.getModel(modelId);
@@ -886,7 +1003,7 @@ public:
return result; return result;
} }
uint16_t getTextureId(AssetsTexture texId) { uint32_t getTextureId(AssetsTexture texId) {
if(texId == 0) if(texId == 0)
return 0; return 0;
@@ -1134,6 +1251,10 @@ class VulkanRenderSession : public IRenderSession {
AtlasImage LightDummy; AtlasImage LightDummy;
Buffer TestQuad; Buffer TestQuad;
std::optional<Buffer> TestVoxel; std::optional<Buffer> TestVoxel;
std::optional<Buffer> AtlasLayersPreview;
uint32_t AtlasLayersPreviewCount = 0;
uint32_t EntityTextureId = 0;
bool EntityTextureReady = false;
VkDescriptorPool DescriptorPool = VK_NULL_HANDLE; VkDescriptorPool DescriptorPool = VK_NULL_HANDLE;
@@ -1187,7 +1308,7 @@ public:
return glm::translate(glm::mat4(quat), camOffset); return glm::translate(glm::mat4(quat), camOffset);
} }
void beforeDraw(); void beforeDraw(double timeSeconds);
void onGpuFinished(); void onGpuFinished();
void drawWorld(GlobalTime gTime, float dTime, VkCommandBuffer drawCmd); void drawWorld(GlobalTime gTime, float dTime, VkCommandBuffer drawCmd);
void pushStage(EnumRenderStage stage); void pushStage(EnumRenderStage stage);
@@ -1195,6 +1316,9 @@ public:
static std::vector<VoxelVertexPoint> generateMeshForVoxelChunks(const std::vector<VoxelCube>& cubes); static std::vector<VoxelVertexPoint> generateMeshForVoxelChunks(const std::vector<VoxelCube>& cubes);
private: private:
void updateTestQuadTexture(uint32_t texId);
void ensureEntityTexture();
void ensureAtlasLayerPreview();
void updateDescriptor_VoxelsLight(); void updateDescriptor_VoxelsLight();
void updateDescriptor_ChunksLight(); void updateDescriptor_ChunksLight();
}; };

View File

@@ -1,4 +1,5 @@
#include "Abstract.hpp" #include "Abstract.hpp"
#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp"
#include "Common/Net.hpp" #include "Common/Net.hpp"
#include "TOSLib.hpp" #include "TOSLib.hpp"
#include <boost/interprocess/file_mapping.hpp> #include <boost/interprocess/file_mapping.hpp>
@@ -6,6 +7,8 @@
#include "boost/json.hpp" #include "boost/json.hpp"
#include "sha2.hpp" #include "sha2.hpp"
#include <algorithm> #include <algorithm>
#include <cctype>
#include <cstring>
#include <boost/iostreams/filtering_streambuf.hpp> #include <boost/iostreams/filtering_streambuf.hpp>
#include <boost/iostreams/copy.hpp> #include <boost/iostreams/copy.hpp>
#include <boost/iostreams/filter/zlib.hpp> #include <boost/iostreams/filter/zlib.hpp>
@@ -15,6 +18,7 @@
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_set>
#include <utility> #include <utility>
@@ -22,6 +26,50 @@ namespace LV {
namespace fs = std::filesystem; namespace fs = std::filesystem;
PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, std::string_view defaultDomain) {
PrecompiledTexturePipeline result;
std::string_view view(cmd);
const size_t trimPos = view.find_first_not_of(" \t\r\n");
if(trimPos == std::string_view::npos)
MAKE_ERROR("Пустая текстурная команда");
view = view.substr(trimPos);
const bool isPipeline = view.size() >= 3
&& view.compare(0, 3, "tex") == 0
&& (view.size() == 3 || std::isspace(static_cast<unsigned char>(view[3])));
if(!isPipeline) {
auto [domain, key] = parseDomainKey(std::string(view), defaultDomain);
result.Assets.emplace_back(std::move(domain), std::move(key));
return result;
}
TexturePipelineProgram program;
std::string err;
if(!program.compile(std::string(view), &err)) {
MAKE_ERROR("Ошибка разбора pipeline: " << err);
}
result.IsSource = true;
result.Pipeline.assign(reinterpret_cast<const char8_t*>(view.data()), view.size());
std::unordered_set<std::string> seen;
for(const auto& patch : program.patches()) {
auto [domain, key] = parseDomainKey(patch.Name, defaultDomain);
std::string token;
token.reserve(domain.size() + key.size() + 1);
token.append(domain);
token.push_back(':');
token.append(key);
if(seen.insert(token).second)
result.Assets.emplace_back(std::move(domain), std::move(key));
}
return result;
}
CompressedVoxels compressVoxels_byte(const std::vector<VoxelCube>& voxels) { CompressedVoxels compressVoxels_byte(const std::vector<VoxelCube>& voxels) {
std::u8string compressed; std::u8string compressed;
@@ -1089,6 +1137,10 @@ uint16_t PreparedNodeState::parseCondition(const std::string_view expression) {
}; };
std::vector<std::variant<EnumTokenKind, std::string_view, int, uint16_t>> tokens; std::vector<std::variant<EnumTokenKind, std::string_view, int, uint16_t>> tokens;
if(expression.empty())
tokens.push_back(int(1));
ssize_t pos = 0; ssize_t pos = 0;
auto skipWS = [&](){ while(pos<expression.size() && std::isspace((unsigned char) expression[pos])) ++pos; }; auto skipWS = [&](){ while(pos<expression.size() && std::isspace((unsigned char) expression[pos])) ++pos; };

View File

@@ -516,6 +516,8 @@ struct PrecompiledTexturePipeline {
std::vector<std::pair<std::string, std::string>> Assets; std::vector<std::pair<std::string, std::string>> Assets;
// Чистый код текстурных преобразований, локальные идентификаторы связаны с Assets // Чистый код текстурных преобразований, локальные идентификаторы связаны с Assets
std::u8string Pipeline; std::u8string Pipeline;
// Pipeline содержит исходный текст (tex ...), нужен для компиляции на сервере
bool IsSource = false;
}; };
struct TexturePipeline { struct TexturePipeline {
@@ -530,15 +532,7 @@ struct TexturePipeline {
}; };
// Компилятор текстурных потоков // Компилятор текстурных потоков
inline PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, const std::string_view defaultDomain = "core") { PrecompiledTexturePipeline compileTexturePipeline(const std::string &cmd, std::string_view defaultDomain = "core");
PrecompiledTexturePipeline result;
auto [domain, key] = parseDomainKey(cmd, defaultDomain);
result.Assets.emplace_back(domain, key);
return result;
}
struct NodestateEntry { struct NodestateEntry {
std::string Name; std::string Name;

View File

@@ -0,0 +1,253 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <stdexcept>
#include <unordered_map>
#include "Common/Async.hpp"
#include "TOSAsync.hpp"
#include "boost/asio/executor.hpp"
#include "boost/asio/experimental/channel.hpp"
#include "boost/asio/this_coro.hpp"
#include "sha2.hpp"
/*
Класс отвечает за отслеживание изменений и подгрузки медиаресурсов в указанных директориях.
Медиаресурсы, собранные из папки assets или зарегистрированные модами.
Хранит все данные в оперативной памяти.
*/
enum class EnumAssets : int {
Nodestate, Particle, Animation, Model, Texture, Sound, Font, MAX_ENUM
};
using AssetsNodestate = uint32_t;
using AssetsParticle = uint32_t;
using AssetsAnimation = uint32_t;
using AssetsModel = uint32_t;
using AssetsTexture = uint32_t;
using AssetsSound = uint32_t;
using AssetsFont = uint32_t;
static constexpr const char* EnumAssetsToDirectory(EnumAssets value) {
switch(value) {
case EnumAssets::Nodestate: return "nodestate";
case EnumAssets::Particle: return "particles";
case EnumAssets::Animation: return "animations";
case EnumAssets::Model: return "models";
case EnumAssets::Texture: return "textures";
case EnumAssets::Sound: return "sounds";
case EnumAssets::Font: return "fonts";
default:
}
assert(!"Неизвестный тип медиаресурса");
}
namespace LV {
namespace fs = std::filesystem;
struct ResourceFile {
using Hash_t = sha2::sha256_hash; // boost::uuids::detail::sha1::digest_type;
Hash_t Hash;
std::vector<std::byte> Data;
void calcHash() {
Hash = sha2::sha256((const uint8_t*) Data.data(), Data.size());
}
};
class AssetsPreloader : public TOS::IAsyncDestructible {
public:
using Ptr = std::shared_ptr<AssetsPreloader>;
//
struct ReloadResult {
};
struct ReloadStatus {
/// TODO: callback'и для обновления статусов
/// TODO: многоуровневый статус std::vector<std::string>. Этапы/Шаги/Объекты
};
public:
static coro<Ptr> Create(asio::io_context& ioc);
~AssetsPreloader() = default;
AssetsPreloader(const AssetsPreloader&) = delete;
AssetsPreloader(AssetsPreloader&&) = delete;
AssetsPreloader& operator=(const AssetsPreloader&) = delete;
AssetsPreloader& operator=(AssetsPreloader&&) = delete;
// Пересматривает ресурсы и выдаёт изменения.
// Одновременно можно работать только один такой вызов.
// instances -> пути к директории с assets или архивы с assets внутри. От низшего приоритета к высшему.
// status -> обратный отклик о процессе обновления ресурсов.
// ReloadStatus <- новые и потерянные ресурсы.
coro<ReloadResult> reloadResources(const std::vector<fs::path>& instances, ReloadStatus* status = nullptr) {
bool expected = false;
assert(Reloading_.compare_exchange_strong(expected, true) && "Двойной вызов reloadResources");
try {
ReloadStatus secondStatus;
co_return _reloadResources(instances, status ? *status : secondStatus);
} catch(...) {
assert(!"reloadResources: здесь не должно быть ошибок");
}
Reloading_.exchange(false);
}
private:
struct ResourceFirstStageInfo {
// Путь к архиву (если есть), и путь до ресурса
fs::path ArchivePath, Path;
// Время изменения файла
fs::file_time_type Timestamp;
};
struct ResourceSecondStageInfo : public ResourceFirstStageInfo {
// Обезличенный ресурс
std::shared_ptr<std::vector<uint8_t>> Resource;
ResourceFile::Hash_t Hash;
// Сырой заголовок
std::vector<std::string> Dependencies;
};
/*
Ресурс имеет бинарную часть, из который вырезаны все зависимости.
Вторая часть это заголовок, которые всегда динамично передаётся с сервера.
В заголовке хранятся зависимости от ресурсов.
*/
struct MediaResource {
std::string Domain, Key;
fs::file_time_type Timestamp;
// Обезличенный ресурс
std::shared_ptr<std::vector<uint8_t>> Resource;
// Хэш ресурса
ResourceFile::Hash_t Hash;
// Скомпилированный заголовок
std::vector<uint8_t> Dependencies;
};
AssetsPreloader(asio::io_context& ioc)
: TOS::IAsyncDestructible(ioc)
{
}
// Текущее состояние reloadResources
std::atomic<bool> Reloading_ = false;
// Пересмотр ресурсов
coro<ReloadResult> _reloadResources(const std::vector<fs::path>& instances, ReloadStatus& status) const {
// 1) Поиск всех ресурсов и построение конечной карты ресурсов (timestamps, path, name, size)
// Карта найденных ресурсов
std::unordered_map<
EnumAssets, // Тип ресурса
std::unordered_map<
std::string, // Domain
std::unordered_map<
std::string, // Key
ResourceFirstStageInfo // ResourceInfo
>
>
> resourcesFirstStage;
for (const fs::path& instance : instances) {
try {
if (fs::is_regular_file(instance)) {
// Может архив
/// TODO: пока не поддерживается
} else if (fs::is_directory(instance)) {
// Директория
fs::path assets = instance / "assets";
if (fs::exists(assets) && fs::is_directory(assets)) {
// Директорию assets существует, перебираем домены в ней
for (auto begin = fs::directory_iterator(assets), end = fs::directory_iterator(); begin != end; begin++) {
if (!begin->is_directory())
continue;
/// TODO: выглядит всё не очень асинхронно
co_await asio::post(co_await asio::this_coro::executor);
fs::path domainPath = begin->path();
std::string domain = domainPath.filename();
// Перебираем по типу ресурса
for (EnumAssets assetType = EnumAssets(0); assetType < EnumAssets::MAX_ENUM; ((int&) assetType)++) {
fs::path assetPath = domainPath / EnumAssetsToDirectory(assetType);
std::unordered_map<
std::string, // Key
ResourceFirstStageInfo // ResourceInfo
>& firstStage = resourcesFirstStage[assetType][domain];
// Исследуем все ресурсы одного типа
for (auto begin = fs::recursive_directory_iterator(assetPath), end = fs::recursive_directory_iterator(); begin != end; begin++) {
if (begin->is_directory())
continue;
fs::path file = begin->path();
std::string key = fs::relative(file, domainPath).string();
// Работаем с ресурсом
firstStage[key] = ResourceFirstStageInfo{
.Path = file,
.Timestamp = fs::last_write_time(file)
};
}
}
}
}
} else {
throw std::runtime_error("Неизвестный тип инстанса медиаресурсов");
}
} catch (const std::exception& exc) {
/// TODO: Логгировать в статусе
}
}
// 2) Обрабатываться будут только изменённые (новый timestamp) или новые ресурсы
// .meta
// Текстуры, шрифты, звуки хранить как есть
// У моделей, состояний нод, анимации, частиц обналичить зависимости
// Мета влияет только на хедер
/// TODO: реализовать реформатирование новых и изменённых ресурсов во внутренний обезличенный формат
co_await asio::post(co_await asio::this_coro::executor);
asio::experimental::channel<void()> ch(IOC, 8);
co_return ReloadResult{};
}
std::unordered_map<
EnumAssets, // Тип ресурса
std::unordered_map<
std::string, // Domain
std::unordered_map<
std::string, // Key
uint32_t // ResourceId
>
>
> DKToId;
std::unordered_map<
EnumAssets, // Тип ресурса
std::unordered_map<
uint32_t,
MediaResource // ResourceInfo
>
> MediaResources;
};
}

View File

@@ -4,6 +4,18 @@
namespace LV::Server { namespace LV::Server {
Entity::Entity(DefEntityId defId)
: DefId(defId)
{
ABBOX = {Pos::Object_t::BS, Pos::Object_t::BS, Pos::Object_t::BS};
WorldId = 0;
Pos = Pos::Object(0);
Speed = Pos::Object(0);
Acceleration = Pos::Object(0);
Quat = glm::quat(1.f, 0.f, 0.f, 0.f);
InRegionPos = Pos::GlobalRegion(0);
}
} }
namespace std { namespace std {
@@ -14,4 +26,4 @@ struct hash<LV::Server::ServerObjectPos> {
return std::hash<uint32_t>()(obj.WorldId) ^ std::hash<LV::Pos::Object>()(obj.ObjectPos); return std::hash<uint32_t>()(obj.WorldId) ^ std::hash<LV::Pos::Object>()(obj.ObjectPos);
} }
}; };
} }

View File

@@ -1,8 +1,10 @@
#include "AssetsManager.hpp" #include "AssetsManager.hpp"
#include "Common/Abstract.hpp" #include "Common/Abstract.hpp"
#include "Client/Vulkan/AtlasPipeline/TexturePipelineProgram.hpp"
#include "boost/json.hpp" #include "boost/json.hpp"
#include "png++/rgb_pixel.hpp" #include "png++/rgb_pixel.hpp"
#include <algorithm> #include <algorithm>
#include <cstring>
#include <exception> #include <exception>
#include <filesystem> #include <filesystem>
#include <png.h> #include <png.h>
@@ -560,10 +562,34 @@ AssetsManager::Out_applyResourceChange AssetsManager::applyResourceChange(const
// Ресолвим текстуры // Ресолвим текстуры
std::variant<LV::PreparedModel, PreparedGLTF> model = _model; std::variant<LV::PreparedModel, PreparedGLTF> model = _model;
std::visit([&lock](auto& val) { std::visit([&lock, &domain](auto& val) {
for(const auto& [key, pipeline] : val.Textures) { for(const auto& [key, pipeline] : val.Textures) {
TexturePipeline pipe; TexturePipeline pipe;
pipe.Pipeline = pipeline.Pipeline; if(pipeline.IsSource) {
std::string source(reinterpret_cast<const char*>(pipeline.Pipeline.data()), pipeline.Pipeline.size());
TexturePipelineProgram program;
std::string err;
if(!program.compile(source, &err)) {
MAKE_ERROR("Ошибка компиляции pipeline: " << err);
}
auto resolver = [&](std::string_view name) -> std::optional<uint32_t> {
auto [texDomain, texKey] = parseDomainKey(std::string(name), domain);
return lock->getId(EnumAssets::Texture, texDomain, texKey);
};
if(!program.link(resolver, &err)) {
MAKE_ERROR("Ошибка линковки pipeline: " << err);
}
const std::vector<uint8_t> bytes = program.toBytes();
pipe.Pipeline.resize(bytes.size());
if(!bytes.empty()) {
std::memcpy(pipe.Pipeline.data(), bytes.data(), bytes.size());
}
} else {
pipe.Pipeline = pipeline.Pipeline;
}
for(const auto& [domain, key] : pipeline.Assets) { for(const auto& [domain, key] : pipeline.Assets) {
ResourceId texId = lock->getId(EnumAssets::Texture, domain, key); ResourceId texId = lock->getId(EnumAssets::Texture, domain, key);

View File

@@ -80,6 +80,16 @@ void ContentManager::registerBase_World(ResourceId id, const std::string& domain
world.emplace(); world.emplace();
} }
void ContentManager::registerBase_Entity(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile) {
std::optional<DefEntity>& entity = getEntry_Entity(id);
if(!entity)
entity.emplace();
DefEntity& def = *entity;
def.Domain = domain;
def.Key = key;
}
void ContentManager::registerBase(EnumDefContent type, const std::string& domain, const std::string& key, const sol::table& profile) void ContentManager::registerBase(EnumDefContent type, const std::string& domain, const std::string& key, const sol::table& profile)
{ {
ResourceId id = getId(type, domain, key); ResourceId id = getId(type, domain, key);
@@ -89,6 +99,8 @@ void ContentManager::registerBase(EnumDefContent type, const std::string& domain
registerBase_Node(id, domain, key, profile); registerBase_Node(id, domain, key, profile);
else if(type == EnumDefContent::World) else if(type == EnumDefContent::World)
registerBase_World(id, domain, key, profile); registerBase_World(id, domain, key, profile);
else if(type == EnumDefContent::Entity)
registerBase_Entity(id, domain, key, profile);
else else
MAKE_ERROR("Не реализовано"); MAKE_ERROR("Не реализовано");
} }

View File

@@ -132,6 +132,7 @@ class ContentManager {
void registerBase_Node(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile); void registerBase_Node(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile);
void registerBase_World(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile); void registerBase_World(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile);
void registerBase_Entity(ResourceId id, const std::string& domain, const std::string& key, const sol::table& profile);
public: public:
ContentManager(AssetsManager &am); ContentManager(AssetsManager &am);
@@ -208,6 +209,10 @@ public:
return std::nullopt; return std::nullopt;
} }
ResourceId getContentId(EnumDefContent type, const std::string& domain, const std::string& key) {
return getId(type, domain, key);
}
private: private:
TOS::Logger LOG = "Server>ContentManager"; TOS::Logger LOG = "Server>ContentManager";
AssetsManager& AM; AssetsManager& AM;

View File

@@ -13,6 +13,7 @@
#include <filesystem> #include <filesystem>
#include <functional> #include <functional>
#include <glm/geometric.hpp> #include <glm/geometric.hpp>
#include <glm/gtc/noise.hpp>
#include <iostream> #include <iostream>
#include <iterator> #include <iterator>
#include <memory> #include <memory>
@@ -853,31 +854,159 @@ void GameServer::BackingAsyncLua_t::run(int id) {
lock->pop(); lock->pop();
} }
//if(key.RegionPos == Pos::GlobalRegion(0, 0, 0)) out.Voxels.clear();
out.Entityes.clear();
{ {
float *ptr = noise.data(); constexpr DefNodeId kNodeAir = 0;
for(int z = 0; z < 64; z++) constexpr DefNodeId kNodeGrass = 2;
for(int y = 0; y < 64; y++) constexpr uint8_t kMetaGrass = 1;
for(int x = 0; x < 64; x++, ptr++) { constexpr DefNodeId kNodeDirt = 3;
DefVoxelId id = std::clamp(*ptr, 0.f, 1.f) * 3; //> 0.9 ? 1 : 0; constexpr DefNodeId kNodeStone = 4;
constexpr DefNodeId kNodeWood = 1;
constexpr DefNodeId kNodeLeaves = 5;
constexpr DefNodeId kNodeLava = 7;
constexpr DefNodeId kNodeWater = 8;
constexpr DefNodeId kNodeFire = 9;
auto hash32 = [](uint32_t x) {
x ^= x >> 16;
x *= 0x7feb352dU;
x ^= x >> 15;
x *= 0x846ca68bU;
x ^= x >> 16;
return x;
};
Pos::GlobalNode regionBase = key.RegionPos;
regionBase <<= 6;
std::array<int, 64*64> heights;
for(int z = 0; z < 64; z++) {
for(int x = 0; x < 64; x++) {
int32_t gx = regionBase.x + x;
int32_t gz = regionBase.z + z;
float fx = float(gx);
float fz = float(gz);
float base = glm::perlin(glm::vec2(fx * 0.005f, fz * 0.005f));
float detail = glm::perlin(glm::vec2(fx * 0.02f, fz * 0.02f)) * 0.35f;
float ridge = glm::perlin(glm::vec2(fx * 0.0015f, fz * 0.0015f));
float ridged = 1.f - std::abs(ridge);
float mountains = ridged * ridged;
float noiseDetail = noise[(z * 64) + x];
float height = 18.f + (base + detail) * 8.f + mountains * 32.f + noiseDetail * 3.f;
int h = std::clamp<int>(int(height + 0.5f), -256, 256);
heights[z * 64 + x] = h;
}
}
for(int z = 0; z < 64; z++) {
for(int x = 0; x < 64; x++) {
int surface = heights[z * 64 + x];
int32_t gx = regionBase.x + x;
int32_t gz = regionBase.z + z;
uint32_t seed = hash32(uint32_t(gx) * 73856093u ^ uint32_t(gz) * 19349663u);
for(int y = 0; y < 64; y++) {
int32_t gy = regionBase.y + y;
Pos::bvec64u nodePos(x, y, z); Pos::bvec64u nodePos(x, y, z);
auto &node = out.Nodes[Pos::bvec4u(nodePos >> 4).pack()][Pos::bvec16u(nodePos & 0xf).pack()]; auto &node = out.Nodes[Pos::bvec4u(nodePos >> 4).pack()][Pos::bvec16u(nodePos & 0xf).pack()];
node.NodeId = id;
if(x == 0 && z == 0)
node.NodeId = 1;
else if(y == 0 && z == 0)
node.NodeId = 2;
else if(x == 0 && y == 0)
node.NodeId = 3;
if(y == 1 && z == 0) if(gy <= surface) {
node.NodeId = 0; if(gy == surface) {
else if(x == 0 && y == 1) node.NodeId = kNodeGrass;
node.NodeId = 0; node.Meta = kMetaGrass;
} else if(gy >= surface - 3) {
node.Meta = uint8_t((x + y + z + int(node.NodeId)) & 0x3); node.NodeId = kNodeDirt;
node.Meta = uint8_t((seed + gy) & 0x3);
} else {
node.NodeId = kNodeStone;
node.Meta = uint8_t((seed + gy + 1) & 0x3);
}
} else {
node.Data = kNodeAir;
}
} }
}
}
auto setNode = [&](int x, int y, int z, DefNodeId id, uint8_t meta, bool onlyAir) {
if(x < 0 || x >= 64 || y < 0 || y >= 64 || z < 0 || z >= 64)
return;
Pos::bvec64u nodePos(x, y, z);
auto &node = out.Nodes[Pos::bvec4u(nodePos >> 4).pack()][Pos::bvec16u(nodePos & 0xf).pack()];
if(onlyAir && node.Data != 0)
return;
node.NodeId = id;
node.Meta = meta;
};
for(int z = 1; z < 63; z++) {
for(int x = 1; x < 63; x++) {
int surface = heights[z * 64 + x];
int localY = surface - regionBase.y;
if(localY < 1 || localY >= 63)
continue;
int32_t gx = regionBase.x + x;
int32_t gz = regionBase.z + z;
uint32_t seed = hash32(uint32_t(gx) * 83492791u ^ uint32_t(gz) * 2971215073u);
int treeHeight = 4 + int(seed % 3);
if(localY + treeHeight + 2 >= 64)
continue;
if((seed % 97) >= 2)
continue;
int diff = surface - heights[z * 64 + (x - 1)];
if(diff > 2 || diff < -2)
continue;
diff = surface - heights[z * 64 + (x + 1)];
if(diff > 2 || diff < -2)
continue;
diff = surface - heights[(z - 1) * 64 + x];
if(diff > 2 || diff < -2)
continue;
diff = surface - heights[(z + 1) * 64 + x];
if(diff > 2 || diff < -2)
continue;
uint8_t woodMeta = uint8_t((seed >> 2) & 0x3);
uint8_t leafMeta = uint8_t((seed >> 4) & 0x3);
for(int i = 1; i <= treeHeight; i++) {
setNode(x, localY + i, z, kNodeWood, woodMeta, false);
}
int topY = localY + treeHeight;
for(int dy = -2; dy <= 2; dy++) {
for(int dz = -2; dz <= 2; dz++) {
for(int dx = -2; dx <= 2; dx++) {
int dist2 = dx * dx + dz * dz + dy * dy;
if(dist2 > 5)
continue;
setNode(x + dx, topY + dy, z + dz, kNodeLeaves, leafMeta, true);
}
}
}
}
}
if(regionBase.x == 0 && regionBase.z == 0) {
constexpr int kTestGlobalY = 64;
if(regionBase.y <= kTestGlobalY && (regionBase.y + 63) >= kTestGlobalY) {
int localY = kTestGlobalY - regionBase.y;
setNode(2, localY, 2, kNodeLava, 0, false);
setNode(4, localY, 2, kNodeWater, 0, false);
setNode(6, localY, 2, kNodeFire, 0, false);
}
}
} }
// else { // else {
// Node *ptr = (Node*) &out.Nodes[0][0]; // Node *ptr = (Node*) &out.Nodes[0][0];
@@ -1447,6 +1576,8 @@ void GameServer::init(fs::path worldPath) {
sol::table t = LuaMainState.create_table(); sol::table t = LuaMainState.create_table();
// Content.CM.registerBase(EnumDefContent::Node, "core", "none", t); // Content.CM.registerBase(EnumDefContent::Node, "core", "none", t);
Content.CM.registerBase(EnumDefContent::World, "test", "devel_world", t); Content.CM.registerBase(EnumDefContent::World, "test", "devel_world", t);
Content.CM.registerBase(EnumDefContent::Entity, "core", "player", t);
PlayerEntityDefId = Content.CM.getContentId(EnumDefContent::Entity, "core", "player");
} }
initLuaPre(); initLuaPre();
@@ -1706,7 +1837,27 @@ void GameServer::stepConnections() {
auto wIter = Expanse.Worlds.find(wPair.first); auto wIter = Expanse.Worlds.find(wPair.first);
assert(wIter != Expanse.Worlds.end()); assert(wIter != Expanse.Worlds.end());
wIter->second->onRemoteClient_RegionsLost(cec, wPair.second); wIter->second->onRemoteClient_RegionsLost(wPair.first, cec, wPair.second);
}
if(cec->PlayerEntity) {
ServerEntityId_t entityId = *cec->PlayerEntity;
auto [worldId, regionPos, entityIndex] = entityId;
auto iterWorld = Expanse.Worlds.find(worldId);
if(iterWorld != Expanse.Worlds.end()) {
auto iterRegion = iterWorld->second->Regions.find(regionPos);
if(iterRegion != iterWorld->second->Regions.end()) {
Region& region = *iterRegion->second;
if(entityIndex < region.Entityes.size())
region.Entityes[entityIndex].IsRemoved = true;
std::vector<ServerEntityId_t> removed = {entityId};
for(const std::shared_ptr<RemoteClient>& observer : region.RMs) {
observer->prepareEntitiesRemove(removed);
}
}
}
cec->clearPlayerEntity();
} }
std::string username = cec->Username; std::string username = cec->Username;
@@ -1811,7 +1962,7 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
auto iterWorld = Expanse.Worlds.find(worldId); auto iterWorld = Expanse.Worlds.find(worldId);
assert(iterWorld != Expanse.Worlds.end()); assert(iterWorld != Expanse.Worlds.end());
std::vector<Pos::GlobalRegion> notLoaded = iterWorld->second->onRemoteClient_RegionsEnter(remoteClient, regions); std::vector<Pos::GlobalRegion> notLoaded = iterWorld->second->onRemoteClient_RegionsEnter(worldId, remoteClient, regions);
if(!notLoaded.empty()) { if(!notLoaded.empty()) {
// Добавляем к списку на загрузку // Добавляем к списку на загрузку
std::vector<Pos::GlobalRegion> &tl = toDB.Load[worldId]; std::vector<Pos::GlobalRegion> &tl = toDB.Load[worldId];
@@ -1824,7 +1975,7 @@ IWorldSaveBackend::TickSyncInfo_Out GameServer::stepDatabaseSync() {
auto iterWorld = Expanse.Worlds.find(worldId); auto iterWorld = Expanse.Worlds.find(worldId);
assert(iterWorld != Expanse.Worlds.end()); assert(iterWorld != Expanse.Worlds.end());
iterWorld->second->onRemoteClient_RegionsLost(remoteClient, regions); iterWorld->second->onRemoteClient_RegionsLost(worldId, remoteClient, regions);
} }
} }
} }
@@ -1929,13 +2080,116 @@ void GameServer::stepGeneratorAndLuaAsync(IWorldSaveBackend::TickSyncInfo_Out db
iterWorld->second->pushRegions(std::move(regions)); iterWorld->second->pushRegions(std::move(regions));
for(auto& [cec, poses] : toSubscribe) { for(auto& [cec, poses] : toSubscribe) {
iterWorld->second->onRemoteClient_RegionsEnter(cec, poses); iterWorld->second->onRemoteClient_RegionsEnter(worldId, cec, poses);
} }
} }
} }
void GameServer::stepPlayerProceed() { void GameServer::stepPlayerProceed() {
auto iterWorld = Expanse.Worlds.find(0);
if(iterWorld == Expanse.Worlds.end())
return;
World& world = *iterWorld->second;
for(std::shared_ptr<RemoteClient>& remoteClient : Game.RemoteClients) {
if(!remoteClient)
continue;
Pos::Object pos = remoteClient->CameraPos;
Pos::GlobalRegion regionPos = Pos::Object_t::asRegionsPos(pos);
glm::quat quat = remoteClient->CameraQuat.toQuat();
if(!remoteClient->PlayerEntity) {
auto iterRegion = world.Regions.find(regionPos);
if(iterRegion == world.Regions.end())
continue;
Entity entity(PlayerEntityDefId);
entity.WorldId = iterWorld->first;
entity.Pos = pos;
entity.Quat = quat;
entity.InRegionPos = regionPos;
Region& region = *iterRegion->second;
RegionEntityId_t entityIndex = region.pushEntity(entity);
if(entityIndex == RegionEntityId_t(-1))
continue;
ServerEntityId_t entityId = {iterWorld->first, regionPos, entityIndex};
remoteClient->setPlayerEntity(entityId);
std::vector<std::tuple<ServerEntityId_t, const Entity*>> updates;
updates.emplace_back(entityId, &region.Entityes[entityIndex]);
for(const std::shared_ptr<RemoteClient>& observer : region.RMs) {
observer->prepareEntitiesUpdate(updates);
}
continue;
}
ServerEntityId_t entityId = *remoteClient->PlayerEntity;
auto [worldId, prevRegion, entityIndex] = entityId;
auto iterRegion = world.Regions.find(prevRegion);
if(iterRegion == world.Regions.end()) {
remoteClient->clearPlayerEntity();
continue;
}
Region& region = *iterRegion->second;
if(entityIndex >= region.Entityes.size() || region.Entityes[entityIndex].IsRemoved) {
remoteClient->clearPlayerEntity();
continue;
}
Entity& entity = region.Entityes[entityIndex];
Pos::GlobalRegion nextRegion = Pos::Object_t::asRegionsPos(pos);
if(nextRegion != prevRegion) {
entity.IsRemoved = true;
std::vector<ServerEntityId_t> removed = {entityId};
for(const std::shared_ptr<RemoteClient>& observer : region.RMs) {
observer->prepareEntitiesRemove(removed);
}
remoteClient->clearPlayerEntity();
auto iterNewRegion = world.Regions.find(nextRegion);
if(iterNewRegion == world.Regions.end())
continue;
Entity nextEntity(PlayerEntityDefId);
nextEntity.WorldId = iterWorld->first;
nextEntity.Pos = pos;
nextEntity.Quat = quat;
nextEntity.InRegionPos = nextRegion;
Region& newRegion = *iterNewRegion->second;
RegionEntityId_t nextIndex = newRegion.pushEntity(nextEntity);
if(nextIndex == RegionEntityId_t(-1))
continue;
ServerEntityId_t nextId = {iterWorld->first, nextRegion, nextIndex};
remoteClient->setPlayerEntity(nextId);
std::vector<std::tuple<ServerEntityId_t, const Entity*>> updates;
updates.emplace_back(nextId, &newRegion.Entityes[nextIndex]);
for(const std::shared_ptr<RemoteClient>& observer : newRegion.RMs) {
observer->prepareEntitiesUpdate(updates);
}
continue;
}
entity.Pos = pos;
entity.Quat = quat;
entity.WorldId = iterWorld->first;
entity.InRegionPos = prevRegion;
std::vector<std::tuple<ServerEntityId_t, const Entity*>> updates;
updates.emplace_back(entityId, &entity);
for(const std::shared_ptr<RemoteClient>& observer : region.RMs) {
observer->prepareEntitiesUpdate(updates);
}
}
} }
void GameServer::stepWorldPhysic() { void GameServer::stepWorldPhysic() {

View File

@@ -267,6 +267,7 @@ class GameServer : public AsyncObject {
// Идентификатор текущегго мода, находящевося в обработке // Идентификатор текущегго мода, находящевося в обработке
std::string CurrentModId; std::string CurrentModId;
AssetsManager::AssetsRegister AssetsInit; AssetsManager::AssetsRegister AssetsInit;
DefEntityId PlayerEntityDefId = 0;
public: public:
GameServer(asio::io_context &ioc, fs::path worldPath); GameServer(asio::io_context &ioc, fs::path worldPath);

View File

@@ -366,10 +366,37 @@ void RemoteClient::NetworkAndResource_t::prepareEntitiesUpdate(const std::vector
} }
} }
// TODO: отправить клиенту ResUses_t::RefEntity_t refEntity;
refEntity.Profile = entity->getDefId();
if(!ResUses.RefDefEntity.contains(refEntity.Profile))
ResUses.RefDefEntity[refEntity.Profile] = {};
checkPacketBorder(32);
NextPacket << (uint8_t) ToClient::L1::Content
<< (uint8_t) ToClient::L2Content::Entity
<< ceId
<< (uint32_t) refEntity.Profile
<< (uint32_t) entity->WorldId
<< entity->Pos.x
<< entity->Pos.y
<< entity->Pos.z;
{
ToServer::PacketQuat q;
q.fromQuat(entity->Quat);
for(int iter = 0; iter < 5; iter++)
NextPacket << q.Data[iter];
}
ResUses.RefEntity[entityId] = std::move(refEntity);
} }
} }
void RemoteClient::NetworkAndResource_t::prepareEntitiesUpdate_Dynamic(const std::vector<std::tuple<ServerEntityId_t, const Entity*>>& entities)
{
prepareEntitiesUpdate(entities);
}
void RemoteClient::NetworkAndResource_t::prepareEntitySwap(ServerEntityId_t prev, ServerEntityId_t next) void RemoteClient::NetworkAndResource_t::prepareEntitySwap(ServerEntityId_t prev, ServerEntityId_t next)
{ {
ReMapEntities.rebindClientKey(prev, next); ReMapEntities.rebindClientKey(prev, next);
@@ -400,6 +427,8 @@ void RemoteClient::NetworkAndResource_t::prepareEntitiesRemove(const std::vector
ResUses.RefDefEntity.erase(iterProfileRef); ResUses.RefDefEntity.erase(iterProfileRef);
ResUses.DefEntity.erase(iterProfile); ResUses.DefEntity.erase(iterProfile);
} }
ResUses.RefEntity.erase(iterEntity);
} }
checkPacketBorder(16); checkPacketBorder(16);
@@ -489,7 +518,12 @@ void RemoteClient::NetworkAndResource_t::prepareWorldRemove(WorldId_t worldId)
// void RemoteClient::NetworkAndResource_t::preparePortalRemove(PortalId portalId) {} // void RemoteClient::NetworkAndResource_t::preparePortalRemove(PortalId portalId) {}
void RemoteClient::prepareCameraSetEntity(ServerEntityId_t entityId) { void RemoteClient::prepareCameraSetEntity(ServerEntityId_t entityId) {
auto lock = NetworkAndResource.lock();
ClientEntityId_t cId = lock->ReMapEntities.toClient(entityId);
lock->checkPacketBorder(8);
lock->NextPacket << (uint8_t) ToClient::L1::System
<< (uint8_t) ToClient::L2System::LinkCameraToEntity
<< cId;
} }
ResourceRequest RemoteClient::pushPreparedPackets() { ResourceRequest RemoteClient::pushPreparedPackets() {
@@ -679,15 +713,19 @@ void RemoteClient::NetworkAndResource_t::informateDefPortal(const std::vector<st
void RemoteClient::NetworkAndResource_t::informateDefEntity(const std::vector<std::pair<DefEntityId, DefEntity*>>& entityes) void RemoteClient::NetworkAndResource_t::informateDefEntity(const std::vector<std::pair<DefEntityId, DefEntity*>>& entityes)
{ {
// for(auto pair : entityes) { for(auto pair : entityes) {
// DefEntityId_t id = pair.first; DefEntityId id = pair.first;
// if(!ResUses.DefEntity.contains(id)) if(!ResUses.DefEntity.contains(id))
// continue; continue;
// NextPacket << (uint8_t) ToClient::L1::Definition checkPacketBorder(8);
// << (uint8_t) ToClient::L2Definition::Entity NextPacket << (uint8_t) ToClient::L1::Definition
// << id; << (uint8_t) ToClient::L2Definition::Entity
// } << id;
if(!ResUses.RefDefEntity.contains(id))
ResUses.RefDefEntity[id] = {};
}
} }
void RemoteClient::NetworkAndResource_t::informateDefItem(const std::vector<std::pair<DefItemId, DefItem*>>& items) void RemoteClient::NetworkAndResource_t::informateDefItem(const std::vector<std::pair<DefItemId, DefItem*>>& items)

View File

@@ -10,6 +10,7 @@
#include <Common/Abstract.hpp> #include <Common/Abstract.hpp>
#include <bitset> #include <bitset>
#include <initializer_list> #include <initializer_list>
#include <optional>
#include <queue> #include <queue>
#include <type_traits> #include <type_traits>
#include <unordered_map> #include <unordered_map>
@@ -336,6 +337,7 @@ public:
// Если игрок пересекал границы региона (для перерасчёта ContentViewState) // Если игрок пересекал границы региона (для перерасчёта ContentViewState)
bool CrossedRegion = true; bool CrossedRegion = true;
std::queue<Pos::GlobalNode> Build, Break; std::queue<Pos::GlobalNode> Build, Break;
std::optional<ServerEntityId_t> PlayerEntity;
public: public:
RemoteClient(asio::io_context &ioc, tcp::socket socket, const std::string username, GameServer* server) RemoteClient(asio::io_context &ioc, tcp::socket socket, const std::string username, GameServer* server)
@@ -347,6 +349,9 @@ public:
coro<> run(); coro<> run();
void shutdown(EnumDisconnect type, const std::string reason); void shutdown(EnumDisconnect type, const std::string reason);
bool isConnected() { return IsConnected; } bool isConnected() { return IsConnected; }
void setPlayerEntity(ServerEntityId_t id) { PlayerEntity = id; }
std::optional<ServerEntityId_t> getPlayerEntity() const { return PlayerEntity; }
void clearPlayerEntity() { PlayerEntity.reset(); }
void pushPackets(std::vector<Net::Packet> *simplePackets, std::vector<Net::SmartPacket> *smartPackets = nullptr) { void pushPackets(std::vector<Net::Packet> *simplePackets, std::vector<Net::SmartPacket> *smartPackets = nullptr) {
if(IsGoingShutdown) if(IsGoingShutdown)

View File

@@ -16,7 +16,7 @@ World::~World() {
} }
std::vector<Pos::GlobalRegion> World::onRemoteClient_RegionsEnter(std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion>& enter) { std::vector<Pos::GlobalRegion> World::onRemoteClient_RegionsEnter(WorldId_t worldId, std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion>& enter) {
std::vector<Pos::GlobalRegion> out; std::vector<Pos::GlobalRegion> out;
for(const Pos::GlobalRegion &pos : enter) { for(const Pos::GlobalRegion &pos : enter) {
@@ -43,18 +43,49 @@ std::vector<Pos::GlobalRegion> World::onRemoteClient_RegionsEnter(std::shared_pt
nodes[Pos::bvec4u(x, y, z)] = region.Nodes[Pos::bvec4u(x, y, z).pack()].data(); nodes[Pos::bvec4u(x, y, z)] = region.Nodes[Pos::bvec4u(x, y, z).pack()].data();
} }
if(!region.Entityes.empty()) {
std::vector<std::tuple<ServerEntityId_t, const Entity*>> updates;
updates.reserve(region.Entityes.size());
for(size_t iter = 0; iter < region.Entityes.size(); iter++) {
const Entity& entity = region.Entityes[iter];
if(entity.IsRemoved)
continue;
ServerEntityId_t entityId = {worldId, pos, static_cast<RegionEntityId_t>(iter)};
updates.emplace_back(entityId, &entity);
}
if(!updates.empty())
cec->prepareEntitiesUpdate(updates);
}
} }
return out; return out;
} }
void World::onRemoteClient_RegionsLost(std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion> &lost) { void World::onRemoteClient_RegionsLost(WorldId_t worldId, std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion> &lost) {
for(const Pos::GlobalRegion &pos : lost) { for(const Pos::GlobalRegion &pos : lost) {
auto region = Regions.find(pos); auto region = Regions.find(pos);
if(region == Regions.end()) if(region == Regions.end())
continue; continue;
if(!region->second->Entityes.empty()) {
std::vector<ServerEntityId_t> removed;
removed.reserve(region->second->Entityes.size());
for(size_t iter = 0; iter < region->second->Entityes.size(); iter++) {
const Entity& entity = region->second->Entityes[iter];
if(entity.IsRemoved)
continue;
removed.emplace_back(worldId, pos, static_cast<RegionEntityId_t>(iter));
}
if(!removed.empty())
cec->prepareEntitiesRemove(removed);
}
std::vector<std::shared_ptr<RemoteClient>> &CECs = region->second->RMs; std::vector<std::shared_ptr<RemoteClient>> &CECs = region->second->RMs;
for(size_t iter = 0; iter < CECs.size(); iter++) { for(size_t iter = 0; iter < CECs.size(); iter++) {
if(CECs[iter] == cec) { if(CECs[iter] == cec) {
@@ -74,6 +105,7 @@ void World::pushRegions(std::vector<std::pair<Pos::GlobalRegion, RegionIn>> regi
Region &region = *(Regions[key] = std::make_unique<Region>()); Region &region = *(Regions[key] = std::make_unique<Region>());
region.Voxels = std::move(value.Voxels); region.Voxels = std::move(value.Voxels);
region.Nodes = value.Nodes; region.Nodes = value.Nodes;
region.Entityes = std::move(value.Entityes);
} }
} }
@@ -81,4 +113,4 @@ void World::onUpdate(GameServer *server, float dtime) {
} }
} }

View File

@@ -146,8 +146,8 @@ public:
Возвращает список не загруженных регионов, на которые соответственно игрока не получилось подписать Возвращает список не загруженных регионов, на которые соответственно игрока не получилось подписать
При подписи происходит отправка всех чанков и сущностей региона При подписи происходит отправка всех чанков и сущностей региона
*/ */
std::vector<Pos::GlobalRegion> onRemoteClient_RegionsEnter(std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion> &enter); std::vector<Pos::GlobalRegion> onRemoteClient_RegionsEnter(WorldId_t worldId, std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion> &enter);
void onRemoteClient_RegionsLost(std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion>& lost); void onRemoteClient_RegionsLost(WorldId_t worldId, std::shared_ptr<RemoteClient> cec, const std::vector<Pos::GlobalRegion>& lost);
struct SaveUnloadInfo { struct SaveUnloadInfo {
std::vector<Pos::GlobalRegion> ToUnload; std::vector<Pos::GlobalRegion> ToUnload;
std::vector<std::pair<Pos::GlobalRegion, SB_Region_In>> ToSave; std::vector<std::pair<Pos::GlobalRegion, SB_Region_In>> ToSave;
@@ -176,4 +176,4 @@ public:
} }

View File

@@ -1,8 +1,11 @@
#include "Common/Abstract.hpp" #include "Common/Abstract.hpp"
#include "boost/asio/awaitable.hpp"
#include <chrono>
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <Client/Vulkan/Vulkan.hpp> #include <Client/Vulkan/Vulkan.hpp>
#include <thread>
namespace LV { namespace LV {
@@ -37,6 +40,4 @@ int main() {
std::cout << "Hello world!" << std::endl; std::cout << "Hello world!" << std::endl;
return LV::main(); return LV::main();
return 0;
} }

View File

@@ -1,6 +1,6 @@
#version 460 #version 460
// layout(early_fragment_tests) in; layout(early_fragment_tests) in;
layout(location = 0) in FragmentObj { layout(location = 0) in FragmentObj {
vec3 GeoPos; // Реальная позиция в мире vec3 GeoPos; // Реальная позиция в мире

View File

@@ -44,4 +44,7 @@ vec4 atlasColor(uint texId, vec2 uv)
void main() { void main() {
Frame = atlasColor(Fragment.Texture, Fragment.UV); Frame = atlasColor(Fragment.Texture, Fragment.UV);
Frame.xyz *= max(0.2f, dot(Fragment.Normal, normalize(vec3(0.5, 1, 0.8)))); Frame.xyz *= max(0.2f, dot(Fragment.Normal, normalize(vec3(0.5, 1, 0.8))));
if(Frame.w == 0)
discard;
} }

161
docs/assets_definitions.md Normal file
View File

@@ -0,0 +1,161 @@
# Определение ресурсов (assets)
Документ описывает формат файлов ресурсов и правила их адресации на стороне сервера.
Описание основано на загрузчиках из `Src/Server/AssetsManager.hpp` и связанных структурах
подготовки (`PreparedNodeState`, `PreparedModel`, `PreparedGLTF`).
## Общая схема
- Ресурсы берутся из списка папок `AssetsRegister::Assets` (от последнего мода к первому).
Первый найденный ресурс по пути имеет приоритет.
- Переопределения через `AssetsRegister::Custom` имеют более высокий приоритет.
- Адрес ресурса состоит из `domain` и `key`.
`domain` — имя папки в assets, `key` — относительный путь внутри папки типа ресурса.
- Обработанные ресурсы сохраняются в `server_cache/assets`.
## Дерево папок
```
assets/
<domain>/
nodestate/ *.json
model/ *.json | *.gltf | *.glb
texture/ *.png | *.jpg (jpeg)
particle/ (загрузка из файлов пока не реализована)
animation/ (загрузка из файлов пока не реализована)
sound/ (загрузка из файлов пока не реализована)
font/ (загрузка из файлов пока не реализована)
```
Пример: `assets/core/nodestate/stone.json` имеет `domain=core`, `key=stone.json`.
При обращении к nodestate из логики нод используется ключ без суффикса `.json`
(сервер дописывает расширение автоматически).
## Nodestate (JSON)
Файл nodestate — это JSON-объект, где ключи — условия, а значения — описание модели
или список вариантов моделей.
### Условия
Условие — строковое выражение. Поддерживаются:
- числа, `true`, `false`
- переменные: `state` или `state:value` (двоеточие — часть имени)
- операторы: `+ - * / %`, `!`, `&`, `|`, `< <= > >= == !=`
- скобки
Пустая строка условия трактуется как `true`.
### Формат варианта модели
Объект варианта:
- `model`: строка `domain:key` **или** массив объектов моделей
- `weight`: число (вес при случайном выборе), по умолчанию `1`
- `uvlock`: bool (используется для векторных моделей; для одиночной модели игнорируется)
- `transformations`: массив строк `"key=value"` для трансформаций
Если `model` — строка, это одиночная модель.
Если `model` — массив, это векторная модель: набор объектов вида:
```
{ "model": "domain:key", "uvlock": false, "transformations": ["x=0", "ry=1.57"] }
```
Для векторной модели также могут задаваться `uvlock` и `transformations` на верхнем уровне
(они применяются к группе).
Трансформации поддерживают ключи:
`x`, `y`, `z`, `rx`, `ry`, `rz` (сдвиг и поворот).
Домен в строке `domain:key` можно опустить — тогда используется домен файла nodestate.
### Пример
```json
{
"": { "model": "core:stone" },
"variant == 1": [
{ "model": "core:stone_alt", "weight": 2 },
{ "model": "core:stone_alt_2", "weight": 1, "transformations": ["ry=1.57"] }
],
"facing:north": {
"model": [
{ "model": "core:stone", "transformations": ["ry=3.14"] },
{ "model": "core:stone_detail", "transformations": ["x=0.5"] }
],
"uvlock": true
}
}
```
## Model (JSON)
Формат описывает геометрию и текстуры.
### Верхний уровень
- `gui_light`: строка (сейчас используется только `default`)
- `ambient_occlusion`: bool
- `display`: объект с наборами `rotation`/`translation`/`scale` (все — массивы из 3 чисел)
- `textures`: объект `name -> string` (ссылка на текстуру или pipeline)
- `cuboids`: массив геометрических блоков
- `sub_models`: массив подмоделей
### Текстуры
В `textures` значение:
- либо строка `domain:key` (прямая ссылка на текстуру),
- либо pipeline-строка, начинающаяся с `tex` (компилируется `TexturePipelineProgram`).
Если домен не указан, используется домен файла модели.
### Cuboids
Элемент `cuboids`:
- `shade`: bool (по умолчанию `true`)
- `from`: `[x, y, z]`
- `to`: `[x, y, z]`
- `faces`: объект граней (`down|up|north|south|west|east`)
- `transformations`: массив `"key=value"` (ключи как у nodestate)
Грань (`faces.<name>`) может содержать:
- `uv`: `[u0, v0, u1, v1]`
- `texture`: строка (ключ из `textures`)
- `cullface`: `down|up|north|south|west|east`
- `tintindex`: int
- `rotation`: int16
### Sub-models
`sub_models` допускает:
- строку `domain:key`
- объект `{ "model": "domain:key", "scene": 0 }`
- объект `{ "path": "domain:key", "scene": 0 }`
Поле `scene` опционально.
### Пример
```json
{
"ambient_occlusion": true,
"textures": {
"all": "core:stone"
},
"cuboids": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"north": { "uv": [0, 0, 16, 16], "texture": "#all" }
}
}
],
"sub_models": [
"core:stone_detail",
{ "model": "core:stone_variant", "scene": 1 }
]
}
```
## Model (glTF / GLB)
Файлы моделей могут быть:
- `.gltf` (JSON glTF)
- `.glb` (binary glTF)
Оба формата конвертируются в `PreparedGLTF`.
## Texture
Поддерживаются только PNG и JPEG.
Формат определяется по сигнатуре файла.
## Прочие типы ресурсов
Для `particle`, `animation`, `sound`, `font` загрузка из файловой системы
в серверном загрузчике пока не реализована (`std::unreachable()`), но возможна
регистрация из Lua через `path` (сырые бинарные данные).