finishing integration of batched-scattered reads for fast load

This commit is contained in:
ecker 2026-07-12 22:12:49 -05:00
parent d1c2de0423
commit 234aba6782
5 changed files with 494 additions and 490 deletions

View File

@ -78,7 +78,7 @@
"stream": {
"tag": "worldspawn",
"player": "info_player_start",
"enabled": false, // "auto",
"enabled": true, // "auto",
"radius": 64,
"every": 1
}

View File

@ -38,6 +38,7 @@ namespace uf {
void UF_API load( const uf::asset::callback_t&, const uf::asset::Payload& );
void UF_API read( const uf::stl::string& filename, size_t offset, size_t length, uint8_t* dest, std::function<void()> callback = {} );
void UF_API read( const uf::stl::string& filename, size_t offset, size_t length, std::function<void(uf::stl::vector<uint8_t>&&)> callback = {} );
void UF_API stream( const uf::stl::string& filename, size_t offset, size_t length, size_t chunkSize, std::function<bool(const uint8_t* data, size_t size, size_t fileOffset)> callback );
uf::stl::string UF_API cache( uf::asset::Payload& );

View File

@ -61,6 +61,7 @@ namespace {
size_t length;
uint8_t* dest;
std::function<void()> callback;
std::function<void(uf::stl::vector<uint8_t>&&)> callbackBuffered;
};
std::mutex mutex;
@ -148,13 +149,26 @@ void uf::asset::processIO() {
for ( auto& [filename, requests] : pendingReads ) {
tasks.queue([filename = filename, requests = std::move(requests)]() {
uf::stl::vector<pod::ScatterRequest> scatterReqs;
scatterReqs.reserve(requests.size());
for ( auto& req : requests ) scatterReqs.emplace_back(pod::ScatterRequest{ req.offset, req.length, req.dest });
uf::stl::vector<pod::ScatterRequest> scatters(requests.size());
uf::stl::vector<uf::stl::vector<uint8_t>> localBuffers(requests.size());
for ( auto i = 0; i < requests.size(); ++i ) {
auto& req = requests[i];
scatters[i] = { req.offset, req.length, req.dest };
if ( !req.dest && req.length > 0 ) {
localBuffers[i].resize( req.length );
scatters[i].dest = localBuffers[i].data();
}
}
uf::io::readScatter( filename, scatterReqs );
uf::io::readScatter( filename, scatters );
for ( auto& req : requests ) if ( req.callback ) req.callback();
for ( auto i = 0; i < requests.size(); ++i ) {
auto& req = requests[i];
if ( req.callback ) req.callback();
else if ( req.callbackBuffered ) req.callbackBuffered( std::move( localBuffers[i] ) );
}
});
}
@ -213,6 +227,10 @@ void uf::asset::read( const uf::stl::string& filename, size_t offset, size_t len
std::lock_guard<std::mutex> lock(::io_read::mutex);
::io_read::queue[filename].emplace_back(::io_read::Job{ offset, length, dest, callback });
}
void uf::asset::read( const uf::stl::string& filename, size_t offset, size_t length, std::function<void(uf::stl::vector<uint8_t>&&)> callback ) {
std::lock_guard<std::mutex> lock(::io_read::mutex);
::io_read::queue[filename].emplace_back(::io_read::Job{ offset, length, nullptr, nullptr, callback });
}
void uf::asset::stream( const uf::stl::string& filename, size_t offset, size_t length, size_t chunkSize, std::function<bool(const uint8_t* data, size_t size, size_t fileOffset)> callback ) {
std::lock_guard<std::mutex> lock(::io_stream::mutex);

View File

@ -37,7 +37,7 @@ namespace {
return 0;
}
uf::Image decodeImage( ext::json::Value& json, pod::Graph& graph, const uf::stl::string& imageName, uf::stl::unordered_map<uf::stl::string, uf::stl::vector<pod::ScatterRequest>>& scatterMap, uf::stl::vector<PendingImage>& pendingImages ) {
uf::Image decodeImage( ext::json::Value& json, pod::Graph& graph, const uf::stl::string& imageName, uf::stl::vector<PendingImage>& pendingImages ) {
uf::Image image;
uf::stl::string filename = "";
@ -94,13 +94,17 @@ namespace {
size_t readLen = length > 0 ? length : uf::io::size( fullPath );
if ( readLen > 0 ) {
pending.buffer.resize(readLen);
scatterMap[fullPath].push_back({
offset,
readLen,
pending.buffer.data()
});
}
pending.buffer.resize(readLen);
uf::asset::read( fullPath, offset, readLen, pending.buffer.data()/*, [&graph, &pending]() {
auto& storage = uf::graph::getStorage(graph);
auto& image = storage.images[pending.name].data;
uf::image::open( image, pending.buffer, pending.extension, false );
uf::image::layers( image, pending.layers );
pending.buffer.clear();
}*/ );
}
}
image.setFilename( fullPath );
@ -201,7 +205,7 @@ namespace {
return skin;
}
uf::Mesh decodeMesh( ext::json::Value& json, pod::Graph& graph, const uf::stl::string& meshName, uf::stl::unordered_map<uf::stl::string, uf::stl::vector<pod::ScatterRequest>>& scatterMap ) {
uf::Mesh decodeMesh( ext::json::Value& json, pod::Graph& graph, const uf::stl::string& meshName ) {
uf::Mesh mesh;
#define DESERIALIZE_MESH(N) {\
@ -264,11 +268,7 @@ namespace {
if ( region.length == 0 ) continue;
mesh.buffers[attr.buffer].resize(region.length);
scatterMap[region.filename].push_back({
region.offset,
region.length,
mesh.buffers[attr.buffer].data()
});
uf::asset::read( region.filename, region.offset, region.length, mesh.buffers[attr.buffer].data() );
}
};
@ -387,6 +387,9 @@ void uf::graph::load( pod::Graph& graph, const uf::stl::string& filename, const
uf::stl::string key = graph.metadata["key"].as<uf::stl::string>("");
if ( key != "" ) key += ":";
uf::stl::vector<PendingImage> pendingImages;
uf::stl::vector<uf::stl::string> meshesToMinify;
tasks.queue([&]{
UF_DEBUG_TIMER_MULTITRACE("Reading material information...");
@ -513,33 +516,18 @@ void uf::graph::load( pod::Graph& graph, const uf::stl::string& filename, const
UF_DEBUG_TIMER_MULTITRACE("Reading images...");
graph.images.reserve( serializer["images"].size() );
uf::stl::vector<PendingImage> pendingImages;
pendingImages.reserve( serializer["images"].size() );
uf::stl::unordered_map<uf::stl::string, uf::stl::vector<pod::ScatterRequest>> scatterMap;
ext::json::forEach( serializer["images"], [&]( ext::json::Value& value ){
auto name = key + value["name"].as<uf::stl::string>();
UF_DEBUG_TIMER_MULTITRACE("Reading image={}", name);
storage.images[name] = {
.data = decodeImage( value, graph, name, scatterMap, pendingImages ),
.data = decodeImage( value, graph, name, pendingImages ),
};
graph.images.emplace_back(name);
});
for ( auto& [filename, requests] : scatterMap ) {
uf::io::readScatter( filename, requests );
}
for ( auto& pending : pendingImages ) {
auto& image = storage.images[pending.name].data;
if ( !pending.buffer.empty() ) {
uf::image::open( image, pending.buffer, pending.extension, false );
uf::image::layers( image, pending.layers );
pending.buffer.clear();
}
}
UF_DEBUG_TIMER_MULTITRACE("Read images");
});
@ -555,29 +543,18 @@ void uf::graph::load( pod::Graph& graph, const uf::stl::string& filename, const
if ( graph.settings.stream.enabled ) preferMinified = false;
uf::stl::unordered_map<uf::stl::string, uf::stl::vector<pod::ScatterRequest>> scatterMap;
uf::stl::vector<uf::stl::string> meshesToMinify;
ext::json::forEach( serializer["meshes"], [&]( ext::json::Value& value ){
auto name = key + value["name"].as<uf::stl::string>();
bool hasMinifiedAsset = value["min"].isObject();
ext::json::Value& json = ( preferMinified && hasMinifiedAsset ) ? value["min"] : value;
storage.meshes[name] = decodeMesh( json, graph, name, scatterMap );
storage.meshes[name] = decodeMesh( json, graph, name );
graph.meshes.emplace_back(name);
if ( preferMinified && !hasMinifiedAsset && !graph.settings.stream.enabled ) {
if ( preferMinified && !hasMinifiedAsset && !graph.settings.stream.enabled )
meshesToMinify.emplace_back( name );
}
});
for ( auto& [filename, requests] : scatterMap ) uf::io::readScatter( filename, requests );
for ( const auto& name : meshesToMinify ) {
auto& mesh = storage.meshes[name];
mesh.prune( { "position", "uv", "st" } );
mesh.convert<float, uint16_t>();
mesh.interleave();
}
UF_DEBUG_TIMER_MULTITRACE("Read meshes");
});
@ -586,7 +563,7 @@ void uf::graph::load( pod::Graph& graph, const uf::stl::string& filename, const
UF_DEBUG_TIMER_MULTITRACE("Reading animation information...");
auto& animNode = serializer["animations"];
if (animNode.isObject()) {
if ( animNode.isObject() ) {
storage.animations.map.reserve( animNode.size() );
ext::json::forEach( animNode, [&]( const uf::stl::string& rawName, ext::json::Value& value ){
auto name = key + rawName;
@ -594,7 +571,7 @@ void uf::graph::load( pod::Graph& graph, const uf::stl::string& filename, const
graph.animations.emplace_back(name);
});
}
else if (animNode.isArray()) {
else if ( animNode.isArray() ) {
storage.animations.map.reserve( animNode.size() );
ext::json::forEach( animNode, [&]( ext::json::Value& value ){
uf::stl::string path = directory + "/" + value.as<uf::stl::string>();
@ -637,6 +614,25 @@ void uf::graph::load( pod::Graph& graph, const uf::stl::string& filename, const
});
uf::thread::execute( tasks );
uf::asset::processIO();
// process images
for ( auto& pending : pendingImages ) {
auto& image = storage.images[pending.name].data;
uf::image::open( image, pending.buffer, pending.extension, false );
uf::image::layers( image, pending.layers );
pending.buffer.clear();
}
// process meshes that need to be minified because I can't easily tie it to the callback
for ( auto& name : meshesToMinify ) {
auto& mesh = storage.meshes[name];
mesh.prune( { "position", "uv", "st" } );
mesh.convert<float, uint16_t>();
mesh.interleave();
}
// re-reference all transform parents
for ( auto& node : graph.nodes ) {

View File

@ -9,6 +9,7 @@
#include <uf/utils/camera/camera.h>
#include <uf/utils/math/physics.h>
#include <uf/utils/memory/map.h>
#include <uf/utils/memory/unordered_set.h>
#include <uf/ext/xatlas/xatlas.h>
#include <uf/ext/ffx/fsr.h>
@ -31,10 +32,11 @@
namespace {
struct PendingTexture {
uf::stl::string key;
uf::stl::string formatHint;
uf::stl::vector<uint8_t> buffer;
bool needsUpload;
};
struct PendingMesh {
uf::stl::unordered_map<size_t, uf::stl::vector<uint8_t>> buffers;
};
struct TextureDescriptor {
@ -2073,446 +2075,17 @@ void uf::graph::destroy( pod::Graph::Storage& storage, bool soft ) {
}
void uf::graph::reload( pod::Graph& graph, pod::Node& node ) {
if ( !(0 <= node.mesh && node.mesh < graph.meshes.size()) ) return;
if ( !node.entity ) return;
auto& scene = uf::scene::getCurrentScene();
auto& storage = uf::graph::getStorage( graph );
auto& graphMetadataJson = graph.metadata;
auto& entity = node.entity->as<uf::Object>();
auto& metadata = entity.getComponent<uf::ObjectBehavior::Metadata>();
auto& metadataJson = entity.getComponent<uf::Serializer>();
auto& transform = entity.getComponent<pod::Transform<>>();
auto& graphMetadataJson = graph.metadata;
ext::json::Value tag = ext::json::find( node.name, graphMetadataJson["tags"] );
pod::Vector3f controllerPosition = {};
auto& controller = scene.getController(); {
auto& controllerTransform = controller.getComponent<pod::Transform<>>();
controllerPosition = controllerTransform.position;
}
/*
if ( controller.getName() != "Scene" || graph.settings.stream.player == -1 ) {
auto& controllerTransform = controller.getComponent<pod::Transform<>>();
controllerPosition = controllerTransform.position;
} else {
// find info_player_spawn
// to-do: deduce the node via tag that attaches the player
for ( auto& node : graph.nodes ) {
if ( node.name != graph.settings.stream.player ) continue;
auto& controllerTransform = node.entity->getComponent<pod::Transform<>>();
controllerPosition = controllerTransform.position;
break;
}
}
*/
bool meshUpdated = false;
auto model = uf::transform::model( transform );
auto& primitives = storage.primitives.map[graph.primitives[node.mesh]];
auto tag = ext::json::find( node.name, graphMetadataJson["tags"] );
auto meshName = graph.meshes[node.mesh];
auto& mesh = storage.meshes.map[meshName];
auto& meshStream = graph.streams.meshes[meshName];
auto& primitives = storage.primitives.map[graph.primitives[node.mesh]];
bool isStreamable = false;
float radius = graph.settings.stream.radius;
float radiusSquared = radius * radius;
// force update if entity isn't already bound to the graphic
if ( !entity.hasComponent<uf::renderer::Graphic>() ) {
meshUpdated = true;
}
// disable if not tagged for streaming
if ( node.index == graph.settings.stream.world ) {
isStreamable = true;
}/* else if ( ext::json::isObject(tag) && tag.has("stream") ) {
auto& streamTag = tag["stream"];
if ( streamTag.has("enabled") && streamTag["enabled"].as<bool>() ) {
isStreamable = true;
radius = streamTag["radius"].as<float>(radius);
}
}*/
if ( !isStreamable ) {
radius = 0;
}
if ( meshStream.buffers.empty() ) {
radius = 0;
}
uf::stl::unordered_set<int32_t> processedBuffers;
if ( radius > 0 && mesh.indirect.count && mesh.indirect.count <= primitives.size() ) {
// deduce draw command (indirect) buffer to write to
auto& attribute = mesh.indirect.attributes.front();
auto& buffer = mesh.buffers[attribute.buffer];
pod::DrawCommand* drawCommands = (pod::DrawCommand*) buffer.data();
// queues
uf::stl::unordered_map<size_t, uf::stl::vector<pod::Range>> ranges;
uf::stl::vector<int8_t> queuedLODs( primitives.size(), -1 ); // this is to maintain draw command order because apparently my code requires draw commands to stay in order
// fallbacks for when no draw calls are requested (mainly for the collision mesh)
float closestDistance = std::numeric_limits<float>::max();
size_t closestDrawID = 0;
bool found = false;
// iterate through meshlets and cull if out of radius
for ( size_t drawID = 0; drawID < primitives.size(); ++drawID ) {
auto& primitive = primitives[drawID];
auto& instance = primitive.instance;
auto& drawCommand = primitive.drawCommand;
pod::Vector3f center = uf::matrix::multiply( model, instance.bounds.center, 1.0f ); // transform the center of the draw call
float distanceSquared = uf::vector::distanceSquared( center, controllerPosition ); // saves a sqrt()
// store closest draw call
if ( distanceSquared < closestDistance ) {
closestDistance = distanceSquared;
closestDrawID = drawID;
}
// queue if we're within the radius
if ( distanceSquared <= radiusSquared ) {
found = true;
int8_t lodLevel = 0;
// deduce a simple ratio [0.0 to 1.0] of how far we are into the streaming radius
float distRatio = distanceSquared / radiusSquared;
if ( distRatio > 0.6f ) lodLevel = 3;
else if ( distRatio > 0.3f ) lodLevel = 2;
else if ( distRatio > 0.1f ) lodLevel = 1;
while ( lodLevel > 0 && primitive.lod.levels[lodLevel].indices == 0 ) {
lodLevel--;
}
queuedLODs[drawID] = lodLevel;
}
}
// insert closest primitive if all are out of range (because of cringe logic)
if ( !found /*&& node.index == graph.settings.stream.world*/ ) {
queuedLODs[closestDrawID] = 3;
}
// bail if no update is detected
auto drawCommandHash = uf::algo::fnv1a(queuedLODs);
graph.settings.stream.lastUpdate = uf::physics::time::current;
if ( drawCommandHash == graph.settings.stream.hash ) {
return;
}
graph.settings.stream.hash = drawCommandHash;
meshUpdated = true;
// read from disk
#if UF_GRAPH_SPARSE_READ_MESH
uint32_t currentVertexCount = 0;
uint32_t currentIndexCount = 0;
struct ActiveDraw {
size_t drawID;
int8_t lodLevel;
uint32_t fileVertexID;
};
static thread_local uf::stl::unordered_map<size_t, uf::stl::vector<pod::Range>> batchedRanges; batchedRanges.clear();
static thread_local uf::stl::unordered_map<size_t, size_t> bufferSizes; bufferSizes.clear();
static thread_local uf::stl::unordered_map<uf::stl::string, uf::stl::vector<pod::ScatterRequest>> scatterMap; scatterMap.clear();
static thread_local uf::stl::unordered_map<size_t, size_t> bufferWriteOffsets; bufferWriteOffsets.clear();
static thread_local uf::stl::unordered_map<size_t, uf::stl::vector<uint8_t>> newBuffers; newBuffers.clear();
STATIC_THREAD_LOCAL(uf::stl::vector<ActiveDraw>, activeDraws);
activeDraws.reserve(queuedLODs.size());
for ( size_t drawID = 0; drawID < queuedLODs.size(); ++drawID ) {
auto lodLevel = queuedLODs[drawID];
if ( lodLevel >= 0 ) {
auto& lod = primitives[drawID].lod.levels[lodLevel];
activeDraws.emplace_back(ActiveDraw{drawID, lodLevel, lod.vertexID});
} else {
auto& drawCommand = drawCommands[drawID];
drawCommand.vertices = 0;
drawCommand.indices = 0;
drawCommand.vertexID = 0;
drawCommand.indexID = 0;
primitives[drawID].drawCommand = drawCommand;
}
}
std::sort(activeDraws.begin(), activeDraws.end(), [](const ActiveDraw& a, const ActiveDraw& b) {
return a.fileVertexID < b.fileVertexID;
});
for ( auto& active : activeDraws ) {
auto& lod = primitives[active.drawID].lod.levels[active.lodLevel];
for ( auto& attr : mesh.index.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
bufferSizes[attr.buffer] += lod.indices * stride;
}
for ( auto& attr : mesh.vertex.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
bufferSizes[attr.buffer] += lod.vertices * stride;
}
}
for ( auto& [b, size] : bufferSizes ) {
newBuffers[b].resize(size);
}
for ( auto& active : activeDraws ) {
auto& primitive = primitives[active.drawID];
auto& lod = primitive.lod.levels[active.lodLevel];
auto& drawCommand = drawCommands[active.drawID];
drawCommand.vertices = lod.vertices;
drawCommand.indices = lod.indices;
drawCommand.vertexID = currentVertexCount;
drawCommand.indexID = currentIndexCount;
primitive.drawCommand = drawCommand;
for ( auto& attr : mesh.index.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
auto& region = meshStream.buffers[attr.buffer];
size_t readBytes = lod.indices * stride;
scatterMap[region.filename].push_back({
region.offset + attr.offset + (lod.indexID * stride),
readBytes,
newBuffers[attr.buffer].data() + bufferWriteOffsets[attr.buffer]
});
bufferWriteOffsets[attr.buffer] += readBytes;
}
for ( auto& attr : mesh.vertex.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
auto& region = meshStream.buffers[attr.buffer];
size_t readBytes = lod.vertices * stride;
scatterMap[region.filename].push_back({
region.offset + attr.offset + (lod.vertexID * stride),
readBytes,
newBuffers[attr.buffer].data() + bufferWriteOffsets[attr.buffer]
});
bufferWriteOffsets[attr.buffer] += readBytes;
}
currentVertexCount += lod.vertices;
currentIndexCount += lod.indices;
}
for ( auto& [filename, requests] : scatterMap ) {
uf::io::readScatter(filename, requests);
}
mesh.vertex.count = currentVertexCount;
mesh.index.count = currentIndexCount;
for ( auto& [b, buf] : newBuffers ) mesh.buffers[b] = std::move(buf);
for ( auto& attr : mesh.vertex.attributes ) attr.offset = 0;
for ( auto& attr : mesh.index.attributes ) attr.offset = 0;
// keep the vertex data intact
#else
// disable remaining draw commands
for ( auto drawID = 0; drawID < primitives.size(); ++drawID ) {
int8_t lodLevel = queuedLODs[drawID];
// reset from LOD0
//primitives[drawID].drawCommand.instances = 1;
primitives[drawID].drawCommand.indices = primitives[drawID].lod.levels[0].indices;
primitives[drawID].drawCommand.indexID = primitives[drawID].lod.levels[0].indexID;
primitives[drawID].drawCommand.vertexID = primitives[drawID].lod.levels[0].vertexID;
primitives[drawID].drawCommand.vertices = primitives[drawID].lod.levels[0].vertices;
// copy from primitive
drawCommands[drawID] = primitives[drawID].drawCommand;
if ( lodLevel < 0 ) {
//drawCommands[drawID].instances = 0;
drawCommands[drawID].vertices = 0;
drawCommands[drawID].indices = 0;
drawCommands[drawID].indexID = 0;
drawCommands[drawID].vertexID = 0;
} else {
auto& lod = primitives[drawID].lod.levels[lodLevel];
drawCommands[drawID].indexID = lod.indexID;
drawCommands[drawID].indices = lod.indices;
drawCommands[drawID].vertexID = lod.vertexID;
drawCommands[drawID].vertices = lod.vertices;
}
// synchronize primitive
primitives[drawID].drawCommand = drawCommands[drawID];
}
#define STREAM_MESH_DATA( N ) \
for ( auto& attribute : mesh.N.attributes ) {\
if ( !mesh.buffers[attribute.buffer].empty() || meshStream.buffers.empty() ) continue;\
auto& region = meshStream.buffers[attribute.buffer];\
uf::io::readAsBuffer( mesh.buffers[attribute.buffer], region.filename, region.offset, region.length );\
}
STREAM_MESH_DATA( index );
STREAM_MESH_DATA( vertex );
#endif
} else {
// load mesh if not already loaded
#define LOAD_MESH_DATA( N ) \
for ( auto& attribute : mesh.N.attributes ) {\
if ( processedBuffers.count(attribute.buffer) ) continue; \
if ( !mesh.buffers[attribute.buffer].empty() || meshStream.buffers.empty() ) continue;\
meshUpdated = true;\
processedBuffers.insert(attribute.buffer); \
auto& region = meshStream.buffers[attribute.buffer];\
uf::io::readAsBuffer( mesh.buffers[attribute.buffer], region.filename, region.offset, region.length );\
}
LOAD_MESH_DATA( index );
LOAD_MESH_DATA( vertex );
}
if ( graph.settings.stream.textures ) {
#define INCREMENT_TEXTURE_REFCOUNT( ID, isSRGB ) if ( 0 <= ID && ID < graph.textures.size() ) {\
auto& key = graph.textures[ID];\
textureDescriptors[key].srgb = isSRGB;\
textureDescriptors[key].references += visible ? 1 : 0;\
textureDescriptors[key].layers = 1;\
}
uf::stl::unordered_map<uf::stl::string, TextureDescriptor> textureDescriptors;
uf::stl::vector<PendingTexture> pendingTextures;
uf::stl::unordered_map<uf::stl::string, uf::stl::vector<pod::ScatterRequest>> textureScatterMap;
pendingTextures.reserve( textureDescriptors.size() );
for ( size_t drawID = 0; drawID < primitives.size(); ++drawID ) {
auto& primitive = primitives[drawID];
auto& instance = primitive.instance;
bool visible = primitive.drawCommand.instances > 0;
INCREMENT_TEXTURE_REFCOUNT(instance.lightmapID, false);
if ( !(0 <= instance.materialID && instance.materialID < graph.materials.size()) ) {
continue;
}
auto& material = storage.materials[graph.materials[instance.materialID]];
INCREMENT_TEXTURE_REFCOUNT(material.indexAlbedo, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexNormal, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexEmissive, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexOcclusion, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexMetallicRoughness, true);
}
// iterate through our ref counts
for ( auto& [ key, descriptor ] : textureDescriptors ) {
auto& image = storage.images[key].data;
auto& texture = storage.images[key].handle;
bool visible = descriptor.references > 0;
if ( visible && (!texture.generated() || texture.aliased) ) {
meshUpdated = true;
pendingTextures.push_back({ key, "", {}, true });
auto& pending = pendingTextures.back();
if ( image.getPixels().empty() ) {
auto& imgStream = graph.streams.images[key];
uf::stl::string formatHint = uf::io::extension(image.getFilename());
if (imgStream.buffer.filename.find(".dtex") != uf::stl::string::npos) formatHint = "dtex";
pending.formatHint = formatHint;
size_t readLen = imgStream.buffer.length > 0 ? imgStream.buffer.length : uf::io::size(imgStream.buffer.filename);
if (readLen > 0) {
pending.buffer.resize(readLen);
textureScatterMap[imgStream.buffer.filename].push_back({
imgStream.buffer.offset,
readLen,
pending.buffer.data()
});
}
}
} else if ( !visible && (texture.generated() && !texture.aliased) ) {
meshUpdated = true;
image.clear();
texture.destroy( true );
texture.aliasTexture(uf::renderer::Texture2D::empty);
}
}
for ( auto& [filename, requests] : textureScatterMap ) {
uf::io::readScatter( filename, requests );
}
for ( auto& pending : pendingTextures ) {
if ( !pending.needsUpload ) continue;
auto& image = storage.images[pending.key].data;
auto& texture = storage.images[pending.key].handle;
auto& descriptor = textureDescriptors[pending.key];
if ( !pending.buffer.empty() ) {
uf::image::open( image, pending.buffer, pending.formatHint, false );
if ( pending.key == "lightmap_atlas" ) {
::convertLightmap( image );
}
pending.buffer.clear();
}
auto filter = uf::renderer::enums::Filter::LINEAR;
auto tag = ext::json::find( pending.key, graphMetadataJson["tags"] );
if ( !ext::json::isObject( tag ) ) {
tag["renderer"] = graphMetadataJson["renderer"];
}
if ( tag["renderer"]["filter"].is<uf::stl::string>() ) {
const auto mode = uf::string::lowercase( tag["renderer"]["filter"].as<uf::stl::string>("linear") );
if ( mode == "linear" ) filter = uf::renderer::enums::Filter::LINEAR;
else if ( mode == "nearest" ) filter = uf::renderer::enums::Filter::NEAREST;
}
if ( texture.aliased ) {
texture.aliased = false;
#if UF_USE_OPENGL
texture.image = 0;
#else
texture.image = {};
texture.view = {};
#endif
}
texture.sampler.descriptor.filter.min = filter;
texture.sampler.descriptor.filter.mag = filter;
texture.layers = descriptor.layers;
texture.srgb = descriptor.srgb;
texture.loadFromImage( image );
#if UF_ENV_DREAMCAST
image.clear();
#endif
}
#undef INCREMENT_TEXTURE_REFCOUNT
}
if ( !meshUpdated ) return;
mesh.updateDescriptor();
// necessary for OpenGL because recorded descriptors have invalidated pointers
// Vulkan doesn't care about the CPU-side mesh data
#if UF_USE_OPENGL
uf::renderer::states::rebuild = true;
// to-do: fix
// mesh.interleave();
#endif
storage.stale = true;
bool graphicOwner = graphMetadataJson["renderer"]["render"].as<bool>();
bool isSkinned = graphMetadataJson["renderer"]["skinned"].as<bool>();
@ -2543,6 +2116,7 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) {
uf::graph::initializeGraphics( graph, entity, mesh, primitives );
}
}
// bind mesh to physics state
{
auto phyziks = tag["physics"];
@ -2575,11 +2149,426 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) {
}
}
void uf::graph::reload( pod::Graph& graph ) {
// update graphics
for ( auto& node : graph.nodes ) uf::graph::reload( graph, node );
auto& scene = uf::scene::getCurrentScene();
auto& storage = uf::graph::getStorage( graph );
auto& graphMetadataJson = graph.metadata;
// setup combined mesh if requested
// ::combineMesh( graph );
pod::Vector3f controllerPosition = {};
auto& controller = scene.getController(); {
auto& controllerTransform = controller.getComponent<pod::Transform<>>();
controllerPosition = controllerTransform.position;
}
uf::stl::unordered_map<int32_t, TextureDescriptor> textureDescriptors;
uf::stl::unordered_set<int32_t> pendingMeshNodes;
uf::stl::unordered_set<int32_t> pendingTextureNodes;
uf::stl::unordered_map<int32_t, PendingTexture> pendingTextures;
uf::stl::unordered_map<int32_t, PendingMesh> pendingMeshes;
auto oldHash = graph.settings.stream.hash;
for ( auto& node : graph.nodes ) {
if ( !(0 <= node.mesh && node.mesh < graph.meshes.size()) ) continue;
if ( !node.entity ) continue;
bool isStreamable = false;
float radius = graph.settings.stream.radius;
float radiusSquared = radius * radius;
auto& entity = node.entity->as<uf::Object>();
auto& metadata = entity.getComponent<uf::ObjectBehavior::Metadata>();
auto& metadataJson = entity.getComponent<uf::Serializer>();
auto& transform = entity.getComponent<pod::Transform<>>();
auto model = uf::transform::model( transform );
auto meshName = graph.meshes[node.mesh];
auto& mesh = storage.meshes.map[meshName];
auto& meshStream = graph.streams.meshes[meshName];
auto& primitives = storage.primitives.map[graph.primitives[node.mesh]];
auto tag = ext::json::find( node.name, graphMetadataJson["tags"] );
// disable if not tagged for streaming
if ( node.index == graph.settings.stream.world ) {
isStreamable = true;
}
if ( !isStreamable ) {
radius = 0;
}
if ( meshStream.buffers.empty() ) {
radius = 0;
}
// force update if entity isn't already bound to the graphic
if ( !entity.hasComponent<uf::renderer::Graphic>() ) {
pendingMeshes[node.mesh] = {};
pendingMeshNodes.insert( node.index );
}
uf::stl::unordered_set<int32_t> processedBuffers;
if ( radius > 0 && mesh.indirect.count && mesh.indirect.count <= primitives.size() ) {
// deduce draw command (indirect) buffer to write to
auto& attribute = mesh.indirect.attributes.front();
auto& buffer = mesh.buffers[attribute.buffer];
pod::DrawCommand* drawCommands = (pod::DrawCommand*) buffer.data();
uf::stl::vector<int8_t> queuedLODs( primitives.size(), -1 ); // this is to maintain draw command order because apparently my code requires draw commands to stay in order
// fallbacks for when no draw calls are requested (mainly for the collision mesh)
float closestDistance = std::numeric_limits<float>::max();
size_t closestDrawID = 0;
bool found = false;
// iterate through meshlets and cull if out of radius
for ( size_t drawID = 0; drawID < primitives.size(); ++drawID ) {
auto& primitive = primitives[drawID];
auto& instance = primitive.instance;
auto& drawCommand = primitive.drawCommand;
pod::Vector3f center = uf::matrix::multiply( model, instance.bounds.center, 1.0f ); // transform the center of the draw call
float distanceSquared = uf::vector::distanceSquared( center, controllerPosition ); // saves a sqrt()
// store closest draw call
if ( distanceSquared < closestDistance ) {
closestDistance = distanceSquared;
closestDrawID = drawID;
}
// queue if we're within the radius
if ( distanceSquared <= radiusSquared ) {
found = true;
int8_t lodLevel = 0;
// deduce a simple ratio [0.0 to 1.0] of how far we are into the streaming radius
float distRatio = distanceSquared / radiusSquared;
if ( distRatio > 0.6f ) lodLevel = 3;
else if ( distRatio > 0.3f ) lodLevel = 2;
else if ( distRatio > 0.1f ) lodLevel = 1;
while ( lodLevel > 0 && primitive.lod.levels[lodLevel].indices == 0 ) {
lodLevel--;
}
queuedLODs[drawID] = lodLevel;
}
}
// insert closest primitive if all are out of range (because of cringe logic)
if ( !found /*&& node.index == graph.settings.stream.world*/ ) {
queuedLODs[closestDrawID] = 3;
}
// bail if no update is detected
auto drawCommandHash = uf::algo::fnv1a(queuedLODs);
graph.settings.stream.lastUpdate = uf::physics::time::current;
if ( drawCommandHash == oldHash ) {
continue;
}
graph.settings.stream.hash = drawCommandHash;
auto& pending = pendingMeshes[node.mesh];
pendingMeshNodes.insert( node.index );
// read from disk
#if UF_GRAPH_SPARSE_READ_MESH
uint32_t currentVertexCount = 0;
uint32_t currentIndexCount = 0;
struct ActiveDraw {
size_t drawID;
int8_t lodLevel;
uint32_t fileVertexID;
};
/*static thread_local*/ uf::stl::unordered_map<size_t, size_t> bufferSizes; /*bufferSizes.clear();*/
/*static thread_local*/ uf::stl::unordered_map<size_t, size_t> bufferWriteOffsets; /*bufferWriteOffsets.clear();*/
STATIC_THREAD_LOCAL(uf::stl::vector<ActiveDraw>, activeDraws);
activeDraws.reserve(queuedLODs.size());
for ( size_t drawID = 0; drawID < queuedLODs.size(); ++drawID ) {
auto lodLevel = queuedLODs[drawID];
if ( lodLevel >= 0 ) {
auto& lod = primitives[drawID].lod.levels[lodLevel];
activeDraws.emplace_back(ActiveDraw{drawID, lodLevel, lod.vertexID});
} else {
auto& drawCommand = drawCommands[drawID];
drawCommand.vertices = 0;
drawCommand.indices = 0;
drawCommand.vertexID = 0;
drawCommand.indexID = 0;
primitives[drawID].drawCommand = drawCommand;
}
}
std::sort(activeDraws.begin(), activeDraws.end(), [](const ActiveDraw& a, const ActiveDraw& b) {
return a.fileVertexID < b.fileVertexID;
});
for ( auto& active : activeDraws ) {
auto& lod = primitives[active.drawID].lod.levels[active.lodLevel];
for ( auto& attr : mesh.index.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
bufferSizes[attr.buffer] += lod.indices * stride;
}
for ( auto& attr : mesh.vertex.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
bufferSizes[attr.buffer] += lod.vertices * stride;
}
}
auto& buffers = mesh.buffers; // to-do: probably deduce when to mesh.buffers vs pending.buffers
for ( auto& [ b, size ] : bufferSizes ) buffers[b].resize( size );
for ( auto& active : activeDraws ) {
auto& primitive = primitives[active.drawID];
auto& lod = primitive.lod.levels[active.lodLevel];
auto& drawCommand = drawCommands[active.drawID];
drawCommand.vertices = lod.vertices;
drawCommand.indices = lod.indices;
drawCommand.vertexID = currentVertexCount;
drawCommand.indexID = currentIndexCount;
primitive.drawCommand = drawCommand;
for ( auto& attr : mesh.index.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
auto& region = meshStream.buffers[attr.buffer];
size_t readBytes = lod.indices * stride;
uf::asset::read( region.filename, region.offset + attr.offset + (lod.indexID * stride), readBytes, buffers[attr.buffer].data() + bufferWriteOffsets[attr.buffer] );
bufferWriteOffsets[attr.buffer] += readBytes;
}
for ( auto& attr : mesh.vertex.attributes ) {
size_t stride = attr.stride > 0 ? attr.stride : attr.descriptor.size;
auto& region = meshStream.buffers[attr.buffer];
size_t readBytes = lod.vertices * stride;
uf::asset::read( region.filename, region.offset + attr.offset + (lod.vertexID * stride), readBytes, buffers[attr.buffer].data() + bufferWriteOffsets[attr.buffer] );
bufferWriteOffsets[attr.buffer] += readBytes;
}
currentVertexCount += lod.vertices;
currentIndexCount += lod.indices;
}
mesh.vertex.count = currentVertexCount;
mesh.index.count = currentIndexCount;
for ( auto& attr : mesh.vertex.attributes ) attr.offset = 0;
for ( auto& attr : mesh.index.attributes ) attr.offset = 0;
// keep the vertex data intact
#else
// disable remaining draw commands
for ( auto drawID = 0; drawID < primitives.size(); ++drawID ) {
int8_t lodLevel = queuedLODs[drawID];
// reset from LOD0
//primitives[drawID].drawCommand.instances = 1;
primitives[drawID].drawCommand.indices = primitives[drawID].lod.levels[0].indices;
primitives[drawID].drawCommand.indexID = primitives[drawID].lod.levels[0].indexID;
primitives[drawID].drawCommand.vertexID = primitives[drawID].lod.levels[0].vertexID;
primitives[drawID].drawCommand.vertices = primitives[drawID].lod.levels[0].vertices;
// copy from primitive
drawCommands[drawID] = primitives[drawID].drawCommand;
if ( lodLevel < 0 ) {
//drawCommands[drawID].instances = 0;
drawCommands[drawID].vertices = 0;
drawCommands[drawID].indices = 0;
drawCommands[drawID].indexID = 0;
drawCommands[drawID].vertexID = 0;
} else {
auto& lod = primitives[drawID].lod.levels[lodLevel];
drawCommands[drawID].indexID = lod.indexID;
drawCommands[drawID].indices = lod.indices;
drawCommands[drawID].vertexID = lod.vertexID;
drawCommands[drawID].vertices = lod.vertices;
}
// synchronize primitive
primitives[drawID].drawCommand = drawCommands[drawID];
}
#define STREAM_MESH_DATA( N ) \
for ( auto& attribute : mesh.N.attributes ) {\
if ( !mesh.buffers[attribute.buffer].empty() || meshStream.buffers.empty() ) continue;\
auto& region = meshStream.buffers[attribute.buffer];\
mesh.buffers[attribute.buffer].resize( region.length );\
uf::asset::read( region.filename, region.offset, region.length, mesh.buffers[attribute.buffer].data() );\
}
STREAM_MESH_DATA( index );
STREAM_MESH_DATA( vertex );
#endif
} else {
// load mesh if not already loaded
#define LOAD_MESH_DATA( N ) \
for ( auto& attribute : mesh.N.attributes ) {\
if ( processedBuffers.count(attribute.buffer) ) continue; \
if ( !mesh.buffers[attribute.buffer].empty() || meshStream.buffers.empty() ) continue;\
pendingMeshes[node.mesh] = {};\
pendingMeshNodes.insert( node.index );\
processedBuffers.insert( attribute.buffer ); \
auto& region = meshStream.buffers[attribute.buffer];\
mesh.buffers[attribute.buffer].resize( region.length );\
uf::asset::read( region.filename, region.offset, region.length, mesh.buffers[attribute.buffer].data() );\
}
LOAD_MESH_DATA( index );
LOAD_MESH_DATA( vertex );
}
if ( graph.settings.stream.textures ) {
#define INCREMENT_TEXTURE_REFCOUNT( ID, isSRGB ) if ( 0 <= ID && ID < graph.textures.size() ) {\
auto& key = graph.textures[ID];\
textureDescriptors[ID].srgb = isSRGB;\
textureDescriptors[ID].references += visible ? 1 : 0;\
textureDescriptors[ID].layers = 1;\
auto& handle = storage.images[key].handle;\
if ( visible && (!handle.generated() || handle.aliased) ) pendingTextureNodes.insert(node.index);\
else if ( !visible && (handle.generated() && !handle.aliased) ) pendingTextureNodes.insert(node.index);\
}
for ( size_t drawID = 0; drawID < primitives.size(); ++drawID ) {
auto& primitive = primitives[drawID];
auto& instance = primitive.instance;
bool visible = primitive.drawCommand.instances > 0;
INCREMENT_TEXTURE_REFCOUNT(instance.lightmapID, false);
if ( !(0 <= instance.materialID && instance.materialID < graph.materials.size()) ) {
continue;
}
auto& material = storage.materials[graph.materials[instance.materialID]];
INCREMENT_TEXTURE_REFCOUNT(material.indexAlbedo, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexNormal, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexEmissive, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexOcclusion, true);
INCREMENT_TEXTURE_REFCOUNT(material.indexMetallicRoughness, true);
}
#undef INCREMENT_TEXTURE_REFCOUNT
}
}
// iterate through our ref counts
for ( auto& [ imageID, descriptor ] : textureDescriptors ) {
auto& key = graph.images[imageID];
auto& image = storage.images[key].data;
auto& texture = storage.images[key].handle;
bool visible = descriptor.references > 0;
if ( visible && (!texture.generated() || texture.aliased) ) {
auto& pending = pendingTextures[imageID];
if ( image.getPixels().empty() ) {
auto& imgStream = graph.streams.images[key];
size_t readLen = imgStream.buffer.length > 0 ? imgStream.buffer.length : uf::io::size(imgStream.buffer.filename);
if ( readLen > 0 ) {
pending.buffer.resize( readLen );
uf::asset::read( imgStream.buffer.filename, imgStream.buffer.offset, readLen, pending.buffer.data() );
}
}
} else if ( !visible && (texture.generated() && !texture.aliased) ) {
image.clear();
texture.destroy( true );
texture.aliasTexture( uf::renderer::Texture2D::empty );
}
}
storage.stale = true;
uf::asset::processIO();
for ( auto& [ imageID, pending ] : pendingTextures ) {
auto& key = graph.images[imageID];
auto& image = storage.images[key].data;
auto& texture = storage.images[key].handle;
auto& descriptor = textureDescriptors[imageID];
auto& imgStream = graph.streams.images[key];
if ( !pending.buffer.empty() ) {
uf::stl::string formatHint = uf::io::extension(image.getFilename());
if ( imgStream.buffer.filename.find(".dtex") != uf::stl::string::npos ) formatHint = "dtex";
uf::image::open( image, pending.buffer, formatHint, false );
if ( key == "lightmap_atlas" ) {
::convertLightmap( image );
}
pending.buffer.clear();
}
auto filter = uf::renderer::enums::Filter::LINEAR;
auto tag = ext::json::find( key, graphMetadataJson["tags"] );
if ( !ext::json::isObject( tag ) ) {
tag["renderer"] = graphMetadataJson["renderer"];
}
if ( tag["renderer"]["filter"].is<uf::stl::string>() ) {
const auto mode = uf::string::lowercase( tag["renderer"]["filter"].as<uf::stl::string>("linear") );
if ( mode == "linear" ) filter = uf::renderer::enums::Filter::LINEAR;
else if ( mode == "nearest" ) filter = uf::renderer::enums::Filter::NEAREST;
}
if ( texture.aliased ) {
texture.aliased = false;
#if UF_USE_OPENGL
texture.image = 0;
#else
texture.image = {};
texture.view = {};
#endif
}
texture.sampler.descriptor.filter.min = filter;
texture.sampler.descriptor.filter.mag = filter;
texture.layers = descriptor.layers;
texture.srgb = descriptor.srgb;
texture.loadFromImage( image );
#if UF_ENV_DREAMCAST
image.clear();
#endif
}
for ( auto& [ meshID, pending ] : pendingMeshes ) {
auto& key = graph.meshes[meshID];
auto& mesh = storage.meshes.map[key];
if ( !pending.buffers.empty() ) {
for ( auto& [b, buf] : pending.buffers ) mesh.buffers[b] = std::move( buf );
}
mesh.updateDescriptor();
// necessary for OpenGL because recorded descriptors have invalidated pointers
// Vulkan doesn't care about the CPU-side mesh data
#if UF_USE_OPENGL
uf::renderer::states::rebuild = true;
#endif
}
for ( auto& nodeID : pendingMeshNodes ) {
auto& node = graph.nodes[nodeID];
uf::graph::reload( graph, node );
}
for ( auto& nodeID : pendingTextureNodes ) {
if ( pendingMeshNodes.count(nodeID) ) continue;
auto& node = graph.nodes[nodeID];
if ( !node.entity || !node.entity->hasComponent<uf::renderer::Graphic>() ) continue;
auto& graphic = node.entity->getComponent<uf::renderer::Graphic>();
::bindTextures( graph, graphic );
}
}
void uf::graph::reload() {
switch ( uf::graph::storageMode ) {