diff --git a/bin/data/entities/model.json b/bin/data/entities/model.json index 9a14bfae..bf1c2934 100644 --- a/bin/data/entities/model.json +++ b/bin/data/entities/model.json @@ -78,7 +78,7 @@ "stream": { "tag": "worldspawn", "player": "info_player_start", - "enabled": false, // "auto", + "enabled": true, // "auto", "radius": 64, "every": 1 } diff --git a/engine/inc/uf/engine/asset/asset.h b/engine/inc/uf/engine/asset/asset.h index d871e783..697e7c0f 100644 --- a/engine/inc/uf/engine/asset/asset.h +++ b/engine/inc/uf/engine/asset/asset.h @@ -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 callback = {} ); + void UF_API read( const uf::stl::string& filename, size_t offset, size_t length, std::function&&)> callback = {} ); void UF_API stream( const uf::stl::string& filename, size_t offset, size_t length, size_t chunkSize, std::function callback ); uf::stl::string UF_API cache( uf::asset::Payload& ); diff --git a/engine/src/engine/asset/asset.cpp b/engine/src/engine/asset/asset.cpp index 96640823..c09cfe39 100644 --- a/engine/src/engine/asset/asset.cpp +++ b/engine/src/engine/asset/asset.cpp @@ -61,6 +61,7 @@ namespace { size_t length; uint8_t* dest; std::function callback; + std::function&&)> 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 scatterReqs; - scatterReqs.reserve(requests.size()); - for ( auto& req : requests ) scatterReqs.emplace_back(pod::ScatterRequest{ req.offset, req.length, req.dest }); + uf::stl::vector scatters(requests.size()); + uf::stl::vector> 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 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&&)> callback ) { + std::lock_guard 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 callback ) { std::lock_guard lock(::io_stream::mutex); diff --git a/engine/src/engine/graph/decode.cpp b/engine/src/engine/graph/decode.cpp index 5e08aade..39633658 100644 --- a/engine/src/engine/graph/decode.cpp +++ b/engine/src/engine/graph/decode.cpp @@ -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>& scatterMap, uf::stl::vector& pendingImages ) { + uf::Image decodeImage( ext::json::Value& json, pod::Graph& graph, const uf::stl::string& imageName, uf::stl::vector& 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>& 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(""); if ( key != "" ) key += ":"; + + uf::stl::vector pendingImages; + uf::stl::vector 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 pendingImages; pendingImages.reserve( serializer["images"].size() ); - uf::stl::unordered_map> scatterMap; ext::json::forEach( serializer["images"], [&]( ext::json::Value& value ){ auto name = key + value["name"].as(); 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> scatterMap; - uf::stl::vector meshesToMinify; - ext::json::forEach( serializer["meshes"], [&]( ext::json::Value& value ){ auto name = key + value["name"].as(); 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(); - 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(); @@ -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(); + mesh.interleave(); + } // re-reference all transform parents for ( auto& node : graph.nodes ) { diff --git a/engine/src/engine/graph/graph.cpp b/engine/src/engine/graph/graph.cpp index f11158da..4876c9b7 100644 --- a/engine/src/engine/graph/graph.cpp +++ b/engine/src/engine/graph/graph.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -31,10 +32,11 @@ namespace { struct PendingTexture { - uf::stl::string key; - uf::stl::string formatHint; uf::stl::vector buffer; - bool needsUpload; + }; + + struct PendingMesh { + uf::stl::unordered_map> 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(); - - auto& metadata = entity.getComponent(); auto& metadataJson = entity.getComponent(); - auto& transform = entity.getComponent>(); - - 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>(); - controllerPosition = controllerTransform.position; - } - - /* - if ( controller.getName() != "Scene" || graph.settings.stream.player == -1 ) { - auto& controllerTransform = controller.getComponent>(); - 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>(); - 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() ) { - 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() ) { - isStreamable = true; - radius = streamTag["radius"].as(radius); - } - }*/ - - if ( !isStreamable ) { - radius = 0; - } - - if ( meshStream.buffers.empty() ) { - radius = 0; - } - - uf::stl::unordered_set 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> ranges; - uf::stl::vector 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::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> batchedRanges; batchedRanges.clear(); - static thread_local uf::stl::unordered_map bufferSizes; bufferSizes.clear(); - static thread_local uf::stl::unordered_map> scatterMap; scatterMap.clear(); - static thread_local uf::stl::unordered_map bufferWriteOffsets; bufferWriteOffsets.clear(); - static thread_local uf::stl::unordered_map> newBuffers; newBuffers.clear(); - - STATIC_THREAD_LOCAL(uf::stl::vector, 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 textureDescriptors; - uf::stl::vector pendingTextures; - uf::stl::unordered_map> 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() ) { - const auto mode = uf::string::lowercase( tag["renderer"]["filter"].as("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 isSkinned = graphMetadataJson["renderer"]["skinned"].as(); @@ -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>(); + controllerPosition = controllerTransform.position; + } + + uf::stl::unordered_map textureDescriptors; + + uf::stl::unordered_set pendingMeshNodes; + uf::stl::unordered_set pendingTextureNodes; + uf::stl::unordered_map pendingTextures; + uf::stl::unordered_map 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(); + + auto& metadata = entity.getComponent(); + auto& metadataJson = entity.getComponent(); + auto& transform = entity.getComponent>(); + + 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() ) { + pendingMeshes[node.mesh] = {}; + pendingMeshNodes.insert( node.index ); + } + + uf::stl::unordered_set 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 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::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 bufferSizes; /*bufferSizes.clear();*/ + /*static thread_local*/ uf::stl::unordered_map bufferWriteOffsets; /*bufferWriteOffsets.clear();*/ + + STATIC_THREAD_LOCAL(uf::stl::vector, 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() ) { + const auto mode = uf::string::lowercase( tag["renderer"]["filter"].as("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() ) continue; + + auto& graphic = node.entity->getComponent(); + ::bindTextures( graph, graphic ); + } } void uf::graph::reload() { switch ( uf::graph::storageMode ) {