diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 15990b2d2636..887b3b3b1fb1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -146,6 +146,7 @@ /modules/lightmapper_rd/ @godotengine/rendering /modules/meshoptimizer/ @godotengine/rendering /modules/raycast/ @godotengine/rendering +/modules/texture_streaming/ @godotengine/rendering /modules/vhacd/ @godotengine/rendering /modules/visual_shader/ @godotengine/shaders /modules/visual_shader/doc_classes/ @godotengine/shaders @godotengine/documentation diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index ac2f4c30d3ad..5126ad5cada2 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -1660,6 +1660,9 @@ The [TextServerManager] singleton. + + The [TextureStreaming] singleton. + The [ThemeDB] singleton. diff --git a/doc/classes/Performance.xml b/doc/classes/Performance.xml index 851832ed697c..8d237314f699 100644 --- a/doc/classes/Performance.xml +++ b/doc/classes/Performance.xml @@ -307,7 +307,10 @@ Number of active navigation obstacles in the [NavigationServer3D]. - + + The amount of memory used by texture streaming (in bytes). This represents the current VRAM usage of streamed textures at their currently loaded mipmap levels. + + Represents the size of the [enum Monitor] enum. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 53f3037f9127..cd2459f08eeb 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -3496,6 +3496,29 @@ If [code]true[/code], the texture importer will import lossless textures using the PNG format. Otherwise, it will default to using WebP. + + If [code]true[/code], enables texture streaming. This system dynamically loads and unloads texture mipmaps based on visibility, reducing VRAM usage. Useful for projects with many large textures. + + + The time (in milliseconds) per mipmap level of quality decay for textures that are no longer visible. When a streamed texture is not requested by the feedback system (e.g., it's off-screen or occluded), its resolution will gradually decrease by one mipmap level for each interval of this duration. + For example, with the default value of [code]5000[/code] ms, a texture that hasn't been visible for 10 seconds will have its quality reduced by 2 mipmap levels. + Set to [code]0[/code] to disable inactivity decay entirely, keeping textures at their last requested resolution indefinitely. + + + The maximum LOD (mipmap level) that streamed textures can load. Lower values allow higher resolution textures but use more VRAM. Valid values are [code]0[/code] (full resolution) to [code]13[/code] (lowest resolution). + + + The maximum number of texture streaming operations (loading/unloading mipmaps) per second. Higher values allow faster adaptation to scene changes but may impact performance. + + + If [code]true[/code], limits the maximum VRAM allocated for streamed textures to the value specified in [member rendering/textures/streaming/memory_budget_mb]. If exceeded, the system reduces the resolution of textures to stay within budget. + + + The maximum VRAM (in megabytes) allocated for streamed textures. If exceeded, the system reduces the resolution of less important textures to stay within budget. + + + The minimum LOD (mipmap level) that streamed textures start at when loaded. Higher values result in lower initial resolution and faster loading. Valid values are [code]0[/code] (full resolution) to [code]13[/code] (lowest resolution). + If [code]true[/code], the GPU texture compressor will cache the local RenderingDevice and its resources (shaders and pipelines), making subsequent imports faster at the cost of increased memory usage. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 5a73ad34ba90..fa010422db12 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -4066,6 +4066,16 @@ Replaces [param texture]'s texture data by the texture specified by the [param by_texture] RID, without changing [param texture]'s RID. + + + + + + Replaces [param texture]'s texture data by the texture specified by the [param by_texture] RID, without changing [param texture]'s RID. + Unlike [method texture_replace], this method automatically patches any uniform sets that reference [param texture] so they point to the new underlying resource. This makes it suitable for replacing textures that are already bound in materials or shaders without invalidating existing draw state. The new texture may have different dimensions (e.g. for texture streaming mip level changes). + Textures that are not compatible will likely cause rendering errors, so it's recommended to only use this method on textures that are compatible with each other (e.g. same format and usage flags). + + diff --git a/doc/classes/ResourceImporterStreamedTexture.xml b/doc/classes/ResourceImporterStreamedTexture.xml new file mode 100644 index 000000000000..821ed711796e --- /dev/null +++ b/doc/classes/ResourceImporterStreamedTexture.xml @@ -0,0 +1,47 @@ + + + + Imports an image as a streaming-capable texture for dynamic mipmap loading. + + + This importer creates [StreamedTexture2D] resources that support dynamic mipmap streaming. When texture streaming is enabled in the Project Settings ([member ProjectSettings.rendering/textures/streaming/enabled]), these textures can have their mipmap levels dynamically loaded and unloaded based on usage, significantly reducing VRAM consumption for projects with many large textures. + The streaming system monitors which textures are visible in each frame and loads appropriate mipmap levels accordingly. Textures that are far from the camera or not currently visible have their higher-resolution mipmap levels unloaded, while textures being displayed up close are loaded at higher resolutions. + For textures that don't need streaming support, use [ResourceImporterTexture] instead. + + + + + + Controls how color channels should be used in the imported texture. + [b]sRGB Friendly:[/b] Prevents the R and RG color formats from being used, as they do not support nonlinear sRGB encoding. + [b]Optimized:[/b] Allows the RG color format to be used if the texture does not use the blue channel. This reduces memory usage if the texture's blue channel can be discarded (all pixels must have a blue value of [code]0[/code]). + + + Controls how VRAM compression should be performed for HDR images. + [b]Disabled:[/b] Never use VRAM compression for HDR textures, regardless of whether they're opaque or transparent. Instead, the texture is converted to RGBE9995 (9-bits per channel + 5-bit exponent = 32 bits per pixel) to reduce memory usage compared to a half-float or single-precision float image format. + [b]Opaque Only:[/b] Only uses VRAM compression for opaque HDR textures. This is due to a limitation of HDR formats, as there is no VRAM-compressed HDR format that supports transparency at the same time. + [b]Always:[/b] Force VRAM compression even for HDR textures with an alpha channel. To perform this, the alpha channel is discarded on import. + [b]Note:[/b] Only effective on Radiance HDR ([code].hdr[/code]) and OpenEXR ([code].exr[/code]) images. + + + If [code]true[/code], uses BPTC compression on desktop platforms and ASTC compression on mobile platforms. When using BPTC, BC7 is used for SDR textures and BC6H is used for HDR textures. + If [code]false[/code], uses the faster but lower-quality S3TC compression on desktop platforms and ETC2 on mobile/web platforms. When using S3TC, DXT1 (BC1) is used for opaque textures and DXT5 (BC3) is used for transparent or normal map (RGTC) textures. + BPTC and ASTC support VRAM compression for HDR textures, but S3TC and ETC2 do not (see [member compress/hdr_compression]). + + + When using a texture as normal map, only the red and green channels are required. Given regular texture compression algorithms produce artifacts that don't look that nice in normal maps, the RGTC compression format is the best fit for this data. Forcing this option to Enable will make Godot import the image as RGTC compressed. By default, it's set to Detect. This means that if the texture is ever detected to be used as a normal map, it will be changed to Enable and reimported automatically. + + + The color channel to consider as a roughness map in this texture. Only effective if Roughness > Src Normal is not empty. + + + The path to the texture to consider as a normal map for roughness filtering on import. Specifying this can help decrease specular aliasing slightly in 3D. + + + The maximum LOD (mipmap level) that can be loaded for this texture. Lower values allow higher resolution. A value of [code]0[/code] uses the global setting from [member ProjectSettings.rendering/textures/streaming/max_lod]. Values [code]1[/code] to [code]14[/code] correspond to LOD levels [code]0[/code] to [code]13[/code]. + + + The minimum LOD (mipmap level) the texture starts at when loaded. Higher values result in lower initial resolution. A value of [code]0[/code] uses the global setting from [member ProjectSettings.rendering/textures/streaming/min_lod]. Values [code]1[/code] to [code]14[/code] correspond to LOD levels [code]0[/code] to [code]13[/code]. + + + diff --git a/doc/classes/StreamedTexture2D.xml b/doc/classes/StreamedTexture2D.xml new file mode 100644 index 000000000000..e0f0eaff00e5 --- /dev/null +++ b/doc/classes/StreamedTexture2D.xml @@ -0,0 +1,34 @@ + + + + A texture that supports dynamic mipmap streaming for optimized VRAM usage. + + + StreamedTexture2D is a texture type that supports dynamic mipmap streaming. When texture streaming is enabled in the Project Settings ([member ProjectSettings.rendering/textures/streaming/enabled]), these textures can have their mipmap levels dynamically loaded and unloaded based on usage. + The texture streaming system monitors which textures are visible in each frame and at what resolution they're being displayed. Textures that are far from the camera or not currently visible have their higher-resolution mipmap levels unloaded to save VRAM, while textures being displayed up close are loaded at higher resolutions. + This texture type is automatically created when importing images with the streaming-compatible importer. + + + + + + + + + Loads the texture from the specified [param path]. + + + + + + The [StreamedTexture2D]'s file path to a [code].stex[/code] file. + + + Overrides the maximum LOD (mipmap level) that can be loaded for this specific texture. Lower values allow higher resolution. A value of [code]0[/code] uses the global setting from [member ProjectSettings.rendering/textures/streaming/max_lod]. Values [code]1[/code] to [code]14[/code] correspond to LOD levels [code]0[/code] to [code]13[/code]. + + + Overrides the minimum LOD (mipmap level) this texture starts at when loaded. Higher values result in lower initial resolution. A value of [code]0[/code] uses the global setting from [member ProjectSettings.rendering/textures/streaming/min_lod]. Values [code]1[/code] to [code]14[/code] correspond to LOD levels [code]0[/code] to [code]13[/code]. + + + + diff --git a/doc/classes/TextureStreaming.xml b/doc/classes/TextureStreaming.xml new file mode 100644 index 000000000000..2b2d01611626 --- /dev/null +++ b/doc/classes/TextureStreaming.xml @@ -0,0 +1,46 @@ + + + + Manages dynamic texture mipmap streaming to optimize VRAM usage. + + + The TextureStreaming singleton manages dynamic loading and unloading of texture mipmap levels based on what is visible in the current frame. This allows projects with many large textures to significantly reduce VRAM usage while maintaining visual quality for visible textures. + Texture streaming works by analyzing shader feedback to determine which textures are actively being used and at what resolution they're being displayed. Textures that are far from the camera or not currently visible have their higher-resolution mipmap levels unloaded, while textures being displayed up close are loaded at higher resolutions. + The system includes a memory budget feature that automatically manages texture resolutions to stay within a specified VRAM limit. When the budget is exceeded, the system prioritizes textures based on how recently they were used and how much screen space they occupy. + [b]Note:[/b] Texture streaming must be enabled in the Project Settings ([member ProjectSettings.rendering/textures/streaming/enabled]) and textures must be imported with streaming support to use this feature. + + + + + + + + Forces all currently queued texture streaming operations to complete immediately, bypassing the normal gradual transition and I/O throttling. This immediately loads or unloads mipmap levels to reach the target resolution for all managed textures, then emits the [signal flush_completed] signal. Useful during loading screens or when teleporting the player to ensure all textures are at their optimal resolution without a visible transition. + + + + + + Returns the current VRAM usage of streamed textures in bytes. This represents the total memory consumed by all streamed textures at their currently loaded mipmap levels. + + + + + + Overrides the maximum LOD (mipmap level) that can be loaded for streamed textures at runtime. Lower values allow higher resolution textures. Valid values are [code]0[/code] (full resolution) to [code]13[/code] (lowest resolution). Values outside that range clear the override and use the default from [member ProjectSettings.rendering/textures/streaming/max_lod]; when no override is active this property returns [code]-1[/code]. + + + Overrides the memory budget for streamed textures at runtime (in megabytes). When the budget is exceeded, the system reduces texture resolution to stay within the limit. A value of [code]UINT32_MAX[/code] ([code]4294967295[/code], the maximum unsigned 32-bit integer) disables the override and uses the default from [member ProjectSettings.rendering/textures/streaming/memory_budget_mb]. + + + Overrides the minimum LOD (mipmap level) that textures start at when loaded. Higher values result in lower initial resolution. Valid values are [code]0[/code] (full resolution) to [code]13[/code] (lowest resolution). Values outside that range clear the override and use the default from [member ProjectSettings.rendering/textures/streaming/min_lod]; when no override is active this property returns [code]-1[/code]. + + + + + + Emitted when a texture streaming flush operation finishes, after [method flush_texture_streaming] has been called and all managed textures have reached their target resolutions. + + + + diff --git a/drivers/d3d12/rendering_device_driver_d3d12.cpp b/drivers/d3d12/rendering_device_driver_d3d12.cpp index 1fa3c8285f5a..a1cdbe0375f4 100644 --- a/drivers/d3d12/rendering_device_driver_d3d12.cpp +++ b/drivers/d3d12/rendering_device_driver_d3d12.cpp @@ -1211,7 +1211,88 @@ void RenderingDeviceDriverD3D12::_discard_texture_subresources(const TextureInfo } } +bool RenderingDeviceDriverD3D12::_data_format_is_compressed(DataFormat p_format) { + switch (p_format) { + case DATA_FORMAT_BC1_RGB_UNORM_BLOCK: + case DATA_FORMAT_BC1_RGB_SRGB_BLOCK: + case DATA_FORMAT_BC1_RGBA_UNORM_BLOCK: + case DATA_FORMAT_BC1_RGBA_SRGB_BLOCK: + case DATA_FORMAT_BC2_UNORM_BLOCK: + case DATA_FORMAT_BC2_SRGB_BLOCK: + case DATA_FORMAT_BC3_UNORM_BLOCK: + case DATA_FORMAT_BC3_SRGB_BLOCK: + case DATA_FORMAT_BC4_UNORM_BLOCK: + case DATA_FORMAT_BC4_SNORM_BLOCK: + case DATA_FORMAT_BC5_UNORM_BLOCK: + case DATA_FORMAT_BC5_SNORM_BLOCK: + case DATA_FORMAT_BC6H_UFLOAT_BLOCK: + case DATA_FORMAT_BC6H_SFLOAT_BLOCK: + case DATA_FORMAT_BC7_UNORM_BLOCK: + case DATA_FORMAT_BC7_SRGB_BLOCK: + case DATA_FORMAT_ETC2_R8G8B8_UNORM_BLOCK: + case DATA_FORMAT_ETC2_R8G8B8_SRGB_BLOCK: + case DATA_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK: + case DATA_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK: + case DATA_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK: + case DATA_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK: + case DATA_FORMAT_EAC_R11_UNORM_BLOCK: + case DATA_FORMAT_EAC_R11_SNORM_BLOCK: + case DATA_FORMAT_EAC_R11G11_UNORM_BLOCK: + case DATA_FORMAT_EAC_R11G11_SNORM_BLOCK: + case DATA_FORMAT_ASTC_4x4_UNORM_BLOCK: + case DATA_FORMAT_ASTC_4x4_SRGB_BLOCK: + case DATA_FORMAT_ASTC_5x4_UNORM_BLOCK: + case DATA_FORMAT_ASTC_5x4_SRGB_BLOCK: + case DATA_FORMAT_ASTC_5x5_UNORM_BLOCK: + case DATA_FORMAT_ASTC_5x5_SRGB_BLOCK: + case DATA_FORMAT_ASTC_6x5_UNORM_BLOCK: + case DATA_FORMAT_ASTC_6x5_SRGB_BLOCK: + case DATA_FORMAT_ASTC_6x6_UNORM_BLOCK: + case DATA_FORMAT_ASTC_6x6_SRGB_BLOCK: + case DATA_FORMAT_ASTC_8x5_UNORM_BLOCK: + case DATA_FORMAT_ASTC_8x5_SRGB_BLOCK: + case DATA_FORMAT_ASTC_8x6_UNORM_BLOCK: + case DATA_FORMAT_ASTC_8x6_SRGB_BLOCK: + case DATA_FORMAT_ASTC_8x8_UNORM_BLOCK: + case DATA_FORMAT_ASTC_8x8_SRGB_BLOCK: + case DATA_FORMAT_ASTC_10x5_UNORM_BLOCK: + case DATA_FORMAT_ASTC_10x5_SRGB_BLOCK: + case DATA_FORMAT_ASTC_10x6_UNORM_BLOCK: + case DATA_FORMAT_ASTC_10x6_SRGB_BLOCK: + case DATA_FORMAT_ASTC_10x8_UNORM_BLOCK: + case DATA_FORMAT_ASTC_10x8_SRGB_BLOCK: + case DATA_FORMAT_ASTC_10x10_UNORM_BLOCK: + case DATA_FORMAT_ASTC_10x10_SRGB_BLOCK: + case DATA_FORMAT_ASTC_12x10_UNORM_BLOCK: + case DATA_FORMAT_ASTC_12x10_SRGB_BLOCK: + case DATA_FORMAT_ASTC_12x12_UNORM_BLOCK: + case DATA_FORMAT_ASTC_12x12_SRGB_BLOCK: + case DATA_FORMAT_ASTC_4x4_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_5x4_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_5x5_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_6x5_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_6x6_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_8x5_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_8x6_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_8x8_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_10x5_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_10x6_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_10x8_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_10x10_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_12x10_SFLOAT_BLOCK: // HDR variant. + case DATA_FORMAT_ASTC_12x12_SFLOAT_BLOCK: // HDR variant. + return true; + default: + return false; + } +} + bool RenderingDeviceDriverD3D12::_unordered_access_supported_by_format(DataFormat p_format) { + // Compressed formats don't support UAV. + if (_data_format_is_compressed(p_format)) { + return false; + } + switch (p_format) { case DATA_FORMAT_R4G4_UNORM_PACK8: case DATA_FORMAT_R4G4B4A4_UNORM_PACK16: @@ -1342,6 +1423,7 @@ RDD::TextureID RenderingDeviceDriverD3D12::texture_create(const TextureFormat &p resource_desc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; } if ((p_format.usage_bits & TEXTURE_USAGE_STORAGE_BIT)) { + ERR_FAIL_COND_V_MSG(_data_format_is_compressed(p_format.format), TextureID(), "Storage textures are not supported for this format."); resource_desc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; } if ((p_format.usage_bits & TEXTURE_USAGE_CPU_READ_BIT)) { @@ -2239,7 +2321,18 @@ static void _rd_stages_and_access_to_d3d12(BitField p_st } } -static D3D12_BARRIER_LAYOUT _rd_texture_layout_to_d3d12_barrier_layout(RDD::TextureLayout p_texture_layout) { +static D3D12_BARRIER_LAYOUT _rd_texture_layout_to_d3d12_barrier_layout(RDD::TextureLayout p_texture_layout, D3D12_COMMAND_LIST_TYPE p_list_type) { + // Copy queues can only use COMMON or UNDEFINED layouts - they don't support COPY_SOURCE/COPY_DEST with enhanced barriers. + if (p_list_type == D3D12_COMMAND_LIST_TYPE_COPY) { + switch (p_texture_layout) { + case RDD::TEXTURE_LAYOUT_UNDEFINED: + return D3D12_BARRIER_LAYOUT_UNDEFINED; + default: + // All other layouts must be COMMON for copy queues. + return D3D12_BARRIER_LAYOUT_COMMON; + } + } + switch (p_texture_layout) { case RDD::TEXTURE_LAYOUT_UNDEFINED: return D3D12_BARRIER_LAYOUT_UNDEFINED; @@ -2291,6 +2384,8 @@ void RenderingDeviceDriverD3D12::command_pipeline_barrier(CommandBufferID p_cmd_ return; } + const CommandBufferInfo *cmd_buf_info = (const CommandBufferInfo *)(p_cmd_buffer.id); + // Convert the RDD barriers to D3D12 enhanced barriers. thread_local LocalVector global_barriers; thread_local LocalVector buffer_barriers; @@ -2332,8 +2427,8 @@ void RenderingDeviceDriverD3D12::command_pipeline_barrier(CommandBufferID p_cmd_ } _rd_stages_and_access_to_d3d12(p_src_stages, texture_barrier_rd.prev_layout, texture_barrier_rd.src_access, texture_barrier_d3d12.SyncBefore, texture_barrier_d3d12.AccessBefore); _rd_stages_and_access_to_d3d12(p_dst_stages, texture_barrier_rd.next_layout, texture_barrier_rd.dst_access, texture_barrier_d3d12.SyncAfter, texture_barrier_d3d12.AccessAfter); - texture_barrier_d3d12.LayoutBefore = _rd_texture_layout_to_d3d12_barrier_layout(texture_barrier_rd.prev_layout); - texture_barrier_d3d12.LayoutAfter = _rd_texture_layout_to_d3d12_barrier_layout(texture_barrier_rd.next_layout); + texture_barrier_d3d12.LayoutBefore = _rd_texture_layout_to_d3d12_barrier_layout(texture_barrier_rd.prev_layout, cmd_buf_info->list_type); + texture_barrier_d3d12.LayoutAfter = _rd_texture_layout_to_d3d12_barrier_layout(texture_barrier_rd.next_layout, cmd_buf_info->list_type); texture_barrier_d3d12.pResource = texture_info->resource; if (texture_barrier_rd.subresources.mipmap_count == texture_info->mipmaps && texture_barrier_rd.subresources.layer_count == texture_info->layers) { // So, all resources. Then, let's be explicit about it so D3D12 doesn't think @@ -2380,7 +2475,6 @@ void RenderingDeviceDriverD3D12::command_pipeline_barrier(CommandBufferID p_cmd_ } if (barrier_groups_count) { - const CommandBufferInfo *cmd_buf_info = (const CommandBufferInfo *)(p_cmd_buffer.id); cmd_buf_info->cmd_list_7->Barrier(barrier_groups_count, barrier_groups); } } @@ -2615,6 +2709,7 @@ RDD::CommandBufferID RenderingDeviceDriverD3D12::command_buffer_create(CommandPo // Bookkeep CommandBufferInfo *cmd_buf_info = VersatileResource::allocate(resources_allocator); + cmd_buf_info->list_type = list_type; cmd_buf_info->cmd_allocator = cmd_allocator; cmd_buf_info->cmd_list = cmd_list; @@ -3915,13 +4010,39 @@ void RenderingDeviceDriverD3D12::command_copy_texture(CommandBufferID p_cmd_buff UINT dst_subresource = _compute_subresource_from_layers(dst_tex_info, p_regions[i].dst_subresources, j); CD3DX12_TEXTURE_COPY_LOCATION src_location(src_tex_info->resource, src_subresource); CD3DX12_TEXTURE_COPY_LOCATION dst_location(dst_tex_info->resource, dst_subresource); - src_box.left = p_regions[i].src_offset.x; - src_box.top = p_regions[i].src_offset.y; - src_box.front = p_regions[i].src_offset.z; - src_box.right = p_regions[i].src_offset.x + p_regions[i].size.x; - src_box.bottom = p_regions[i].src_offset.y + p_regions[i].size.y; - src_box.back = p_regions[i].src_offset.z + p_regions[i].size.z; - cmd_buf_info->cmd_list->CopyTextureRegion(&dst_location, p_regions[i].dst_offset.x, p_regions[i].dst_offset.y, p_regions[i].dst_offset.z, &src_location, &src_box); + + // Detect whole-subresource copies so we can pass nullptr instead of an + // explicit box. Some D3D12 drivers reject full-box copies on the + // smallest block-compressed mips. I think this is because for compressed formats you need copy + // at block granularity, and the box calculation may end up with an invalid box. + // Copying the full resource allows it to calculate the box itself and avoid the issue. + const uint32_t src_mip_abs = src_tex_info->base_mip + p_regions[i].src_subresources.mipmap; + const uint32_t src_sub_width = MAX(1U, uint32_t(src_tex_info->desc.Width >> src_mip_abs)); + const uint32_t src_sub_height = (src_tex_info->desc.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE1D) + ? 1U + : MAX(1U, uint32_t(src_tex_info->desc.Height) >> src_mip_abs); + const uint32_t src_sub_depth = (src_tex_info->desc.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D) + ? MAX(1U, uint32_t(src_tex_info->desc.DepthOrArraySize) >> src_mip_abs) + : 1U; + + const bool full_subresource = + p_regions[i].src_offset == Vector3i() && + p_regions[i].dst_offset == Vector3i() && + p_regions[i].size.x == int32_t(src_sub_width) && + p_regions[i].size.y == int32_t(src_sub_height) && + p_regions[i].size.z == int32_t(src_sub_depth); + + if (full_subresource) { + cmd_buf_info->cmd_list->CopyTextureRegion(&dst_location, 0, 0, 0, &src_location, nullptr); + } else { + src_box.left = p_regions[i].src_offset.x; + src_box.top = p_regions[i].src_offset.y; + src_box.front = p_regions[i].src_offset.z; + src_box.right = p_regions[i].src_offset.x + p_regions[i].size.x; + src_box.bottom = p_regions[i].src_offset.y + p_regions[i].size.y; + src_box.back = p_regions[i].src_offset.z + p_regions[i].size.z; + cmd_buf_info->cmd_list->CopyTextureRegion(&dst_location, p_regions[i].dst_offset.x, p_regions[i].dst_offset.y, p_regions[i].dst_offset.z, &src_location, &src_box); + } } } } diff --git a/drivers/d3d12/rendering_device_driver_d3d12.h b/drivers/d3d12/rendering_device_driver_d3d12.h index 36639117f0d7..0129180ef3e0 100644 --- a/drivers/d3d12/rendering_device_driver_d3d12.h +++ b/drivers/d3d12/rendering_device_driver_d3d12.h @@ -317,6 +317,8 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver { void _discard_texture_subresources(const TextureInfo *p_tex_info, const CommandBufferInfo *p_cmd_buf_info); + bool _data_format_is_compressed(DataFormat p_format); + protected: virtual bool _unordered_access_supported_by_format(DataFormat p_format); @@ -486,6 +488,7 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver { // Store a self list reference to be used by the command pool. SelfList command_buffer_info_elem{ this }; + D3D12_COMMAND_LIST_TYPE list_type = D3D12_COMMAND_LIST_TYPE_DIRECT; Microsoft::WRL::ComPtr cmd_allocator; Microsoft::WRL::ComPtr cmd_list; Microsoft::WRL::ComPtr cmd_list_1; diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp index faa52c4f47c7..a0b6e3333540 100644 --- a/drivers/gles3/storage/material_storage.cpp +++ b/drivers/gles3/storage/material_storage.cpp @@ -1374,6 +1374,7 @@ MaterialStorage::MaterialStorage() { actions.usage_defines["AO"] = "#define AO_USED\n"; actions.usage_defines["AO_LIGHT_AFFECT"] = "#define AO_USED\n"; actions.usage_defines["UV"] = "#define UV_USED\n"; + actions.usage_defines["STREAMING_UV"] = "#define STREAMING_UV_USED\n"; actions.usage_defines["UV2"] = "#define UV2_USED\n"; actions.usage_defines["BONE_INDICES"] = "#define BONES_USED\n"; actions.usage_defines["BONE_WEIGHTS"] = "#define WEIGHTS_USED\n"; diff --git a/drivers/gles3/storage/texture_storage.h b/drivers/gles3/storage/texture_storage.h index 5a640b93a3f8..74e58de0ee88 100644 --- a/drivers/gles3/storage/texture_storage.h +++ b/drivers/gles3/storage/texture_storage.h @@ -697,6 +697,9 @@ class TextureStorage : public RendererTextureStorage { virtual RID texture_drawable_get_default_material() const override; virtual void texture_replace(RID p_texture, RID p_by_texture) override; + virtual void texture_replace_compatible(RID p_texture, RID p_by_texture) override { + texture_replace(p_texture, p_by_texture); + } virtual void texture_set_size_override(RID p_texture, int p_width, int p_height) override; virtual void texture_set_path(RID p_texture, const String &p_path) override; @@ -937,6 +940,8 @@ class TextureStorage : public RendererTextureStorage { } String get_framebuffer_error(GLenum p_status); + + virtual void texture_2d_attach_streaming_state(RID p_texture, RID p_streaming_state) override {} }; inline String TextureStorage::get_framebuffer_error(GLenum p_status) { diff --git a/editor/docks/filesystem_dock.cpp b/editor/docks/filesystem_dock.cpp index 3d0e4a970b03..1f0d20a5a14a 100644 --- a/editor/docks/filesystem_dock.cpp +++ b/editor/docks/filesystem_dock.cpp @@ -2303,7 +2303,7 @@ void FileSystemDock::_file_option(int p_option, const Vector &p_selected external_program = EDITOR_GET("text_editor/external/exec_path"); } else if (extension == "res" || extension == "scn") { // Binary resources have no meaningful editor outside Godot, so just fallback to something default. - } else if (resource_type == "CompressedTexture2D" || resource_type == "Image") { + } else if (resource_type == "CompressedTexture2D" || resource_type == "StreamedTexture2D" || resource_type == "Image") { if (extension == "svg" || extension == "svgz") { external_program = EDITOR_GET("filesystem/external_programs/vector_image_editor"); } else { diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 851ef0f1c214..56cc1eb1279c 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -106,6 +106,7 @@ #include "editor/import/resource_importer_imagefont.h" #include "editor/import/resource_importer_layered_texture.h" #include "editor/import/resource_importer_shader_file.h" +#include "editor/import/resource_importer_streamed_texture.h" #include "editor/import/resource_importer_svg.h" #include "editor/import/resource_importer_texture.h" #include "editor/import/resource_importer_texture_atlas.h" @@ -602,6 +603,7 @@ void EditorNode::_update_from_settings() { } ResourceImporterTexture::get_singleton()->update_imports(); + ResourceImporterStreamedTexture::get_singleton()->update_imports(); _update_translations(); @@ -952,6 +954,7 @@ void EditorNode::_notification(int p_what) { editor_selection->update(); ResourceImporterTexture::get_singleton()->update_imports(); + ResourceImporterStreamedTexture::get_singleton()->update_imports(); if (requested_first_scan) { requested_first_scan = false; @@ -8587,9 +8590,14 @@ EditorNode::EditorNode() { { // Register importers at the beginning, so dialogs are created with the right extensions. - Ref import_texture = memnew(ResourceImporterTexture(true)); + Ref import_texture; + import_texture.instantiate(true); ResourceFormatImporter::get_singleton()->add_importer(import_texture); + Ref import_streamed_texture; + import_streamed_texture.instantiate(true); + ResourceFormatImporter::get_singleton()->add_importer(import_streamed_texture); + Ref import_cubemap; import_cubemap.instantiate(); import_cubemap->set_mode(ResourceImporterLayeredTexture::MODE_CUBEMAP); diff --git a/editor/import/resource_importer_streamed_texture.cpp b/editor/import/resource_importer_streamed_texture.cpp new file mode 100644 index 000000000000..f5217ef1c15b --- /dev/null +++ b/editor/import/resource_importer_streamed_texture.cpp @@ -0,0 +1,332 @@ +/**************************************************************************/ +/* resource_importer_streamed_texture.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "resource_importer_streamed_texture.h" + +#include "core/io/config_file.h" +#include "core/io/image_loader.h" +#include "editor/file_system/editor_file_system.h" +#include "editor/import/resource_importer_texture_settings.h" +#include "scene/resources/streamed_texture.h" + +ResourceImporterStreamedTexture *ResourceImporterStreamedTexture::singleton = nullptr; + +void ResourceImporterStreamedTexture::_texture_reimport_roughness(const Ref &p_tex, const String &p_normal_path, RSE::TextureDetectRoughnessChannel p_channel) { + ERR_FAIL_COND(p_tex.is_null()); + + MutexLock lock(singleton->mutex); + StringName path = p_tex->get_path(); + + if (!singleton->make_flags.has(path)) { + singleton->make_flags[path] = MakeInfo(); + } + + singleton->make_flags[path].flags |= MAKE_ROUGHNESS_FLAG; + singleton->make_flags[path].channel_for_roughness = p_channel; + singleton->make_flags[path].normal_path_for_roughness = p_normal_path; +} + +void ResourceImporterStreamedTexture::_texture_reimport_normal(const Ref &p_tex) { + ERR_FAIL_COND(p_tex.is_null()); + + MutexLock lock(singleton->mutex); + StringName path = p_tex->get_path(); + + if (!singleton->make_flags.has(path)) { + singleton->make_flags[path] = MakeInfo(); + } + + singleton->make_flags[path].flags |= MAKE_NORMAL_FLAG; +} + +String ResourceImporterStreamedTexture::get_importer_name() const { + return "streamed_texture_2d"; +} +String ResourceImporterStreamedTexture::get_visible_name() const { + return "Texture2D Streamed"; +} +void ResourceImporterStreamedTexture::get_recognized_extensions(List *p_extensions) const { + ImageLoader::get_recognized_extensions(p_extensions); +} + +String ResourceImporterStreamedTexture::get_save_extension() const { + return "stex"; +} +String ResourceImporterStreamedTexture::get_resource_type() const { + return "StreamedTexture2D"; +} + +void ResourceImporterStreamedTexture::get_import_options(const String &p_path, List *r_options, int p_preset) const { + r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "compress/high_quality"), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/hdr_compression", PROPERTY_HINT_ENUM, "Disabled,Opaque Only,Always"), 1)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/normal_map", PROPERTY_HINT_ENUM, "Detect,Enable,Disabled"), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/channel_pack", PROPERTY_HINT_ENUM, "sRGB Friendly,Optimized"), 0)); + + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "roughness/mode", PROPERTY_HINT_ENUM, "Detect,Disabled,Red,Green,Blue,Alpha,Gray"), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "roughness/src_normal", PROPERTY_HINT_FILE, "*.bmp,*.exr,*.jpeg,*.jpg,*.hdr,*.png,*.svg,*.tga,*.webp"), "")); + + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "streaming/min_lod_override", PROPERTY_HINT_ENUM, "Settings,0,1,2,3,4,5,6,7,8,9,10,11,12,13"), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "streaming/max_lod_override", PROPERTY_HINT_ENUM, "Settings,0,1,2,3,4,5,6,7,8,9,10,11,12,13"), 0)); +} + +bool ResourceImporterStreamedTexture::get_option_visibility(const String &p_path, const String &p_option, const HashMap &p_options) const { + return true; +} + +Error ResourceImporterStreamedTexture::import(ResourceUID::ID p_source_id, const String &p_source_file, const String &p_save_path, const HashMap &p_options, List *r_platform_variants, List *r_gen_files, Variant *r_metadata) { + Ref image; + Error err; + image.instantiate(); + + uint32_t save_flags = 0; + Image::UsedChannels used_channels = Image::USED_CHANNELS_RGBA; + + // Compression options. + const bool high_quality = p_options.has("compress/high_quality") && bool(p_options["compress/high_quality"]); + const int hdr_compression = p_options.has("compress/hdr_compression") ? int(p_options["compress/hdr_compression"]) : 1; + const int pack_channels = p_options.has("compress/channel_pack") ? int(p_options["compress/channel_pack"]) : 0; + const bool srgb_friendly_pack = pack_channels == 0; + + Image::CompressSource comp_source = srgb_friendly_pack ? Image::COMPRESS_SOURCE_SRGB : Image::COMPRESS_SOURCE_GENERIC; + + err = ImageLoader::load_image(p_source_file, image); + + // Roughness. + const int roughness = p_options["roughness/mode"]; + const bool detect_roughness = roughness == 0; + + // Normal map. + const String normal_map = p_options["roughness/src_normal"]; + const int normal = p_options["compress/normal_map"]; + const bool detect_normal = normal == 0; // Normal is set to Detect + const bool force_normal = normal == 1; // Normal is set to Enable + + if (detect_normal || force_normal) { + save_flags |= StreamedTexture2D::FORMAT_BIT_DETECT_NORMAL; + } + + if (detect_roughness) { + save_flags |= StreamedTexture2D::FORMAT_BIT_DETECT_ROUGHNESS; + } + + if (force_normal) { + comp_source = Image::COMPRESS_SOURCE_NORMAL; + } + + // Load the normal image. + Ref normal_image; + Image::RoughnessChannel roughness_channel = Image::ROUGHNESS_CHANNEL_R; + + if (roughness > 1 && FileAccess::exists(normal_map)) { + normal_image.instantiate(); + if (ImageLoader::load_image(normal_map, normal_image) == OK) { + roughness_channel = Image::RoughnessChannel(roughness - 2); + } + } + + if (!image->has_mipmaps() || force_normal) { + image->generate_mipmaps(force_normal); + } + + // Generate roughness mipmaps from normal texture. + if (image->has_mipmaps() && normal_image.is_valid()) { + image->generate_mipmap_roughness(roughness_channel, normal_image); + } + + if (err != OK || image.is_null() || image->is_empty()) { + return ERR_CANT_OPEN; + } + + // Detect used channels for optimal compression (after image is fully loaded/processed). + if (!image->is_compressed()) { + used_channels = image->detect_used_channels(comp_source); + } + + Array formats_imported; + + // Streaming lod range overrides. + const uint32_t streaming_min = p_options.has("streaming/min_lod_override") ? uint32_t(p_options["streaming/min_lod_override"]) : 0; + const uint32_t streaming_max = p_options.has("streaming/max_lod_override") ? uint32_t(p_options["streaming/max_lod_override"]) : 0; + + const bool can_s3tc_bptc = ResourceImporterTextureSettings::should_import_s3tc_bptc(); + const bool can_etc2_astc = ResourceImporterTextureSettings::should_import_etc2_astc(); + ERR_FAIL_COND_V_MSG(!can_s3tc_bptc && !can_etc2_astc, FAILED, "No supported compression formats are enabled in the project settings for streamed textures."); + + // HDR handling. + const bool is_hdr = (image->get_format() >= Image::FORMAT_RF && image->get_format() <= Image::FORMAT_RGBE9995); + bool can_compress_hdr = hdr_compression > 0; + bool force_uncompressed = false; + + if (is_hdr) { + bool has_alpha = image->detect_alpha() != Image::ALPHA_NONE; + if (has_alpha) { + // HDR with alpha is not compressible to BC6H/ASTC-HDR. + if (hdr_compression == 2) { + // User selected "Always", so force an alpha-less format. + if (image->get_format() == Image::FORMAT_RGBAF) { + image->convert(Image::FORMAT_RGBF); + } else if (image->get_format() == Image::FORMAT_RGBAH) { + image->convert(Image::FORMAT_RGBH); + } + } else { + can_compress_hdr = false; + } + } + + // Fall back to RGBE9995 uncompressed if HDR compression is disabled. + if (!can_compress_hdr && image->get_format() != Image::FORMAT_RGBE9995) { + image->convert(Image::FORMAT_RGBE9995); + force_uncompressed = true; + } + } + + if (force_uncompressed) { + // Save uncompressed (no platform variants needed). + Error err_unc = StreamedTexture2D::_save_data(p_save_path + ".stex", image, save_flags, streaming_min, streaming_max); + ERR_FAIL_COND_V_MSG(err_unc != OK, err_unc, "Failed to save uncompressed HDR streamed texture."); + } else { + if (can_s3tc_bptc) { + formats_imported.push_back("s3tc_bptc"); + Image::CompressMode image_compress_mode; + String image_compress_format; + if (high_quality || is_hdr) { + image_compress_mode = Image::COMPRESS_BPTC; + image_compress_format = "bptc"; + } else { + image_compress_mode = Image::COMPRESS_S3TC; + image_compress_format = "s3tc"; + } + Ref image_s3tc_bptc = image->duplicate(); + image_s3tc_bptc->compress_from_channels(image_compress_mode, used_channels); + Error err_s3tc = StreamedTexture2D::_save_data(p_save_path + "." + image_compress_format + ".stex", image_s3tc_bptc, save_flags, streaming_min, streaming_max); + ERR_FAIL_COND_V_MSG(err_s3tc != OK, err_s3tc, "Failed to save S3TC/BPTC streamed texture."); + if (err_s3tc == OK) { + r_platform_variants->push_back(image_compress_format); + } + } + + if (can_etc2_astc) { + formats_imported.push_back("etc2_astc"); + Image::CompressMode image_compress_mode; + String image_compress_format; + if (high_quality || is_hdr) { + image_compress_mode = Image::COMPRESS_ASTC; + image_compress_format = "astc"; + } else { + image_compress_mode = Image::COMPRESS_ETC2; + image_compress_format = "etc2"; + } + Ref image_etc2_astc = image->duplicate(); + image_etc2_astc->compress_from_channels(image_compress_mode, used_channels); + Error err_etc2 = StreamedTexture2D::_save_data(p_save_path + "." + image_compress_format + ".stex", image_etc2_astc, save_flags, streaming_min, streaming_max); + ERR_FAIL_COND_V_MSG(err_etc2 != OK, err_etc2, "Failed to save ETC2/ASTC streamed texture."); + if (err_etc2 == OK) { + r_platform_variants->push_back(image_compress_format); + } + } + } + + if (r_metadata) { + Dictionary meta; + meta["vram_texture"] = true; + + if (formats_imported.size()) { + meta["imported_formats"] = formats_imported; + } + + *r_metadata = meta; + } + + return OK; +} + +ResourceImporterStreamedTexture::ResourceImporterStreamedTexture(bool p_singleton) { + // This should only be set through the EditorNode. + if (p_singleton) { + singleton = this; + } + + StreamedTexture2D::request_roughness_callback = _texture_reimport_roughness; + StreamedTexture2D::request_normal_callback = _texture_reimport_normal; +} + +void ResourceImporterStreamedTexture::update_imports() { + if (EditorFileSystem::get_singleton()->is_scanning() || EditorFileSystem::get_singleton()->is_importing()) { + return; // Don't update when EditorFileSystem is doing something else. + } + + MutexLock lock(mutex); + Vector to_reimport; + + if (make_flags.is_empty()) { + return; + } + + for (const KeyValue &E : make_flags) { + Ref cf; + cf.instantiate(); + String src_path = String(E.key) + ".import"; + + Error err = cf->load(src_path); + ERR_CONTINUE(err != OK); + + bool changed = false; + + if (E.value.flags & MAKE_NORMAL_FLAG && int(cf->get_value("params", "compress/normal_map")) == 0) { + print_line( + vformat("%s: Texture detected as used as a normal map in 3D. Enabling red-green texture compression to reduce memory usage (blue channel is discarded).", + String(E.key))); + + cf->set_value("params", "compress/normal_map", 1); + changed = true; + } + + if (E.value.flags & MAKE_ROUGHNESS_FLAG && int(cf->get_value("params", "roughness/mode")) == 0) { + print_line( + vformat("%s: Texture detected as used as a roughness map in 3D. Enabling roughness limiter based on the detected associated normal map at %s.", + String(E.key), E.value.normal_path_for_roughness)); + + cf->set_value("params", "roughness/mode", E.value.channel_for_roughness + 2); + cf->set_value("params", "roughness/src_normal", E.value.normal_path_for_roughness); + changed = true; + } + + if (changed) { + cf->save(src_path); + to_reimport.push_back(E.key); + } + } + + make_flags.clear(); + + if (!to_reimport.is_empty()) { + EditorFileSystem::get_singleton()->reimport_files(to_reimport); + } +} diff --git a/editor/import/resource_importer_streamed_texture.h b/editor/import/resource_importer_streamed_texture.h new file mode 100644 index 000000000000..2be1e9c8e648 --- /dev/null +++ b/editor/import/resource_importer_streamed_texture.h @@ -0,0 +1,80 @@ +/**************************************************************************/ +/* resource_importer_streamed_texture.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/io/resource_importer.h" +#include "servers/rendering/rendering_server_enums.h" + +class StreamedTexture2D; + +class ResourceImporterStreamedTexture : public ResourceImporter { + GDCLASS(ResourceImporterStreamedTexture, ResourceImporter); + + static ResourceImporterStreamedTexture *singleton; + + enum { + MAKE_ROUGHNESS_FLAG = 1, + MAKE_NORMAL_FLAG = 2, + }; + + Mutex mutex; + struct MakeInfo { + int flags = 0; + String normal_path_for_roughness; + RSE::TextureDetectRoughnessChannel channel_for_roughness = RSE::TEXTURE_DETECT_ROUGHNESS_R; + }; + + HashMap make_flags; + + static void _texture_reimport_roughness(const Ref &p_tex, const String &p_normal_path, RSE::TextureDetectRoughnessChannel p_channel); + static void _texture_reimport_normal(const Ref &p_tex); + +public: + static ResourceImporterStreamedTexture *get_singleton() { return singleton; } + + virtual String get_importer_name() const override; + virtual String get_visible_name() const override; + virtual void get_recognized_extensions(List *p_extensions) const override; + virtual String get_save_extension() const override; + virtual String get_resource_type() const override; + virtual float get_priority() const override { + return -1.0; // Set lower than resource_importer_texture + } + + void update_imports(); + + virtual void get_import_options(const String &p_path, List *r_options, int p_preset = 0) const override; + virtual bool get_option_visibility(const String &p_path, const String &p_option, const HashMap &p_options) const override; + virtual Error import(ResourceUID::ID p_source_id, const String &p_source_file, const String &p_save_path, const HashMap &p_options, List *r_platform_variants, List *r_gen_files = nullptr, Variant *r_metadata = nullptr) override; + + ResourceImporterStreamedTexture(bool p_singleton = false); + virtual ~ResourceImporterStreamedTexture() = default; +}; diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp index 21c473917ced..436b19784a8f 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -67,6 +67,7 @@ #include "editor/import/resource_importer_imagefont.h" #include "editor/import/resource_importer_layered_texture.h" #include "editor/import/resource_importer_shader_file.h" +#include "editor/import/resource_importer_streamed_texture.h" #include "editor/import/resource_importer_svg.h" #include "editor/import/resource_importer_texture.h" #include "editor/import/resource_importer_texture_atlas.h" @@ -221,6 +222,7 @@ void register_editor_types() { GDREGISTER_CLASS(ResourceImporterTexture); GDREGISTER_CLASS(ResourceImporterTextureAtlas); GDREGISTER_CLASS(ResourceImporterWAV); + GDREGISTER_CLASS(ResourceImporterStreamedTexture); // This list is alphabetized, and plugins that depend on Node2D or Node3D are in their own section below. EditorPlugins::add_by_type(); diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp index a3e23fc2141f..03d814d07ea3 100644 --- a/editor/scene/3d/node_3d_editor_plugin.cpp +++ b/editor/scene/3d/node_3d_editor_plugin.cpp @@ -92,11 +92,14 @@ #include "scene/3d/physics/collision_shape_3d.h" #include "scene/3d/physics/physics_body_3d.h" #include "scene/3d/world_environment.h" +#include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/center_container.h" +#include "scene/gui/check_box.h" #include "scene/gui/color_picker.h" #include "scene/gui/flow_container.h" #include "scene/gui/menu_button.h" +#include "scene/gui/popup.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/separator.h" #include "scene/gui/spin_box.h" @@ -108,6 +111,10 @@ #include "servers/physics_3d/physics_server_3d_types.h" #include "servers/rendering/rendering_server.h" +#ifdef MODULE_TEXTURE_STREAMING_ENABLED +#include "modules/texture_streaming/texture_streaming.h" +#endif + using namespace Node3DEditorConstants; /////////////////////////////////////////////////////////////////// @@ -2312,6 +2319,164 @@ void Node3DEditor::_sun_environ_settings_pressed() { sun_environ_popup->grab_focus(); } +#ifdef MODULE_TEXTURE_STREAMING_ENABLED +void Node3DEditor::_textures_button_pressed() { + if (textures_button->is_disabled()) { + return; + } + + Vector2 pos = textures_button->get_screen_position(); + pos.y += +textures_button->get_size().y; + textures_popup->set_position(pos); + textures_popup->reset_size(); + textures_popup->popup(); +} + +void Node3DEditor::_textures_button_update_state() { + const bool texture_streaming_enabled = GLOBAL_GET("rendering/textures/streaming/enabled"); + textures_button->set_disabled(!texture_streaming_enabled); + + if (!texture_streaming_enabled) { + textures_popup->hide(); + } +} + +void Node3DEditor::_textures_preset_pressed(int p_preset) { + textures_very_low->set_pressed(false); + textures_low->set_pressed(false); + textures_medium->set_pressed(false); + textures_high->set_pressed(false); + textures_very_high->set_pressed(false); + textures_max->set_pressed(false); + + switch (p_preset) { + case TEXTURE_QUALITY_VERY_LOW: + textures_very_low->set_pressed(true); + textures_min_lod_slider->set_value(4); + textures_max_lod_slider->set_value(8); + break; + case TEXTURE_QUALITY_LOW: + textures_low->set_pressed(true); + textures_min_lod_slider->set_value(3); + textures_max_lod_slider->set_value(7); + break; + case TEXTURE_QUALITY_MEDIUM: + textures_medium->set_pressed(true); + textures_min_lod_slider->set_value(2); + textures_max_lod_slider->set_value(6); + break; + case TEXTURE_QUALITY_HIGH: + textures_high->set_pressed(true); + textures_min_lod_slider->set_value(1); + textures_max_lod_slider->set_value(5); + break; + case TEXTURE_QUALITY_VERY_HIGH: + textures_very_high->set_pressed(true); + textures_min_lod_slider->set_value(0); + textures_max_lod_slider->set_value(4); + break; + case TEXTURE_QUALITY_MAX: + textures_max->set_pressed(true); + textures_min_lod_slider->set_value(0); + textures_max_lod_slider->set_value(0); + break; + } +} + +void Node3DEditor::_textures_max_lod_changed(float p_value) { + float max_value = textures_min_lod_slider->get_value(); + if (p_value < max_value) { + textures_min_lod_slider->set_value(p_value); + } + _textures_apply_settings(); +} +void Node3DEditor::_textures_min_lod_changed(float p_value) { + float min_value = textures_max_lod_slider->get_value(); + if (p_value > min_value) { + textures_max_lod_slider->set_value(p_value); + } + _textures_apply_settings(); +} + +void Node3DEditor::_textures_budget_toggled(bool p_enabled) { + if (p_enabled) { + // textures_budget_enable->set_pressed(true); + textures_budget_slider->set_read_only(false); + } else { + // textures_budget_enable->set_pressed(false); + textures_budget_slider->set_read_only(true); + } + + _textures_apply_settings(); +} + +void Node3DEditor::_textures_budget_changed(float p_value) { + _textures_apply_settings(); +} + +void Node3DEditor::_textures_save_pressed() { + ProjectSettings::get_singleton()->set_setting("rendering/textures/streaming/min_lod", int(textures_min_lod_slider->get_value())); + ProjectSettings::get_singleton()->set_setting("rendering/textures/streaming/max_lod", int(textures_max_lod_slider->get_value())); + ProjectSettings::get_singleton()->set_setting("rendering/textures/streaming/memory_budget_mb", int(textures_budget_slider->get_value())); + const bool budget_enabled = textures_budget_enable->is_pressed(); + ProjectSettings::get_singleton()->set_setting("rendering/textures/streaming/memory_budget_enabled", budget_enabled); + ProjectSettings::get_singleton()->save(); + textures_popup->hide(); +} + +void Node3DEditor::_textures_load_settings() { + int min_lod = GLOBAL_GET("rendering/textures/streaming/min_lod"); + int max_lod = GLOBAL_GET("rendering/textures/streaming/max_lod"); + int budget_size = GLOBAL_GET("rendering/textures/streaming/memory_budget_mb"); + bool budget_enabled = GLOBAL_GET("rendering/textures/streaming/memory_budget_enabled"); + + textures_max_lod_slider->set_value(max_lod); + textures_min_lod_slider->set_value(min_lod); + textures_budget_slider->set_value(budget_size); + textures_budget_enable->set_pressed(budget_enabled); + + _textures_max_lod_changed(textures_max_lod_slider->get_value()); + _textures_min_lod_changed(textures_min_lod_slider->get_value()); + _textures_budget_changed(textures_budget_slider->get_value()); + + textures_very_low->set_pressed(false); + textures_low->set_pressed(false); + textures_medium->set_pressed(false); + textures_high->set_pressed(false); + textures_very_high->set_pressed(false); + textures_max->set_pressed(false); + + if (min_lod == 0 && max_lod == 0) { + textures_max->set_pressed(true); + } else if (min_lod == 0 && max_lod == 4) { + textures_very_high->set_pressed(true); + } else if (min_lod == 1 && max_lod == 5) { + textures_high->set_pressed(true); + } else if (min_lod == 2 && max_lod == 6) { + textures_medium->set_pressed(true); + } else if (min_lod == 3 && max_lod == 7) { + textures_low->set_pressed(true); + } else if (min_lod == 4 && max_lod == 8) { + textures_very_low->set_pressed(true); + } +} + +void Node3DEditor::_textures_apply_settings() { + int min_lod = textures_min_lod_slider->get_value(); + int max_lod = textures_max_lod_slider->get_value(); + + TextureStreaming::get_singleton()->set_min_lod_override(min_lod); + TextureStreaming::get_singleton()->set_max_lod_override(max_lod); + const int budget_size = textures_budget_slider->get_value(); + const bool budget_enabled = textures_budget_enable->is_pressed(); + if (budget_enabled) { + TextureStreaming::get_singleton()->set_memory_budget_mb_override(budget_size); + } else { + TextureStreaming::get_singleton()->set_memory_budget_mb_override(0); + } +} +#endif + void Node3DEditor::_add_sun_to_scene(bool p_already_added_environment) { sun_environ_popup->hide(); @@ -2441,6 +2606,10 @@ void Node3DEditor::_notification(int p_what) { _update_preview_environment(); +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + _textures_button_update_state(); +#endif + sun_state->set_custom_minimum_size(sun_vb->get_combined_minimum_size()); environ_state->set_custom_minimum_size(environ_vb->get_combined_minimum_size()); @@ -3509,9 +3678,19 @@ Node3DEditor::Node3DEditor() { view_layout_menu->set_switch_on_hover(true); view_layout_menu->set_shortcut_context(this); transform_view_hbox->add_child(view_layout_menu); - transform_view_hbox->add_child(memnew(VSeparator)); +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + textures_button = memnew(Button); + textures_button->set_text(TTRC("Textures")); + textures_button->set_tooltip_text(TTRC("Edit texture streaming quality settings.")); + textures_button->set_theme_type_variation(SceneStringName(FlatButton)); + textures_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_button_pressed)); + + main_flow->add_child(textures_button); + main_flow->add_child(memnew(VSeparator)); +#endif + context_toolbar_panel = memnew(PanelContainer); context_toolbar_hbox = memnew(HBoxContainer); context_toolbar_panel->add_child(context_toolbar_hbox); @@ -3921,6 +4100,120 @@ void fragment() { _load_default_preview_settings(); _preview_settings_changed(); } + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + { + textures_popup = memnew(PopupPanel); + add_child(textures_popup); + + VBoxContainer *textures_vb = memnew(VBoxContainer); + textures_vb->set_custom_minimum_size(Size2(200 * EDSCALE, 0)); + textures_vb->add_theme_constant_override("separation", 10); + textures_popup->add_child(textures_vb); + + // Presets + HBoxContainer *presets_hb = memnew(HBoxContainer); + presets_hb->set_h_size_flags(SIZE_EXPAND_FILL); + textures_very_low = memnew(Button); + textures_very_low->set_text(TTRC("Very Low")); + textures_very_low->set_theme_type_variation(SceneStringName(FlatButton)); + textures_very_low->set_h_size_flags(SIZE_EXPAND_FILL); + textures_very_low->set_toggle_mode(true); + textures_very_low->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_preset_pressed).bind(TextureQualityPreset::TEXTURE_QUALITY_VERY_LOW), CONNECT_DEFERRED); + presets_hb->add_child(textures_very_low); + + textures_low = memnew(Button); + textures_low->set_text(TTRC("Low")); + textures_low->set_theme_type_variation(SceneStringName(FlatButton)); + textures_low->set_h_size_flags(SIZE_EXPAND_FILL); + textures_low->set_toggle_mode(true); + textures_low->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_preset_pressed).bind(TextureQualityPreset::TEXTURE_QUALITY_LOW), CONNECT_DEFERRED); + presets_hb->add_child(textures_low); + + textures_medium = memnew(Button); + textures_medium->set_text(TTRC("Medium")); + textures_medium->set_theme_type_variation(SceneStringName(FlatButton)); + textures_medium->set_h_size_flags(SIZE_EXPAND_FILL); + textures_medium->set_toggle_mode(true); + textures_medium->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_preset_pressed).bind(TextureQualityPreset::TEXTURE_QUALITY_MEDIUM), CONNECT_DEFERRED); + presets_hb->add_child(textures_medium); + + textures_high = memnew(Button); + textures_high->set_text(TTRC("High")); + textures_high->set_theme_type_variation(SceneStringName(FlatButton)); + textures_high->set_h_size_flags(SIZE_EXPAND_FILL); + textures_high->set_toggle_mode(true); + textures_high->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_preset_pressed).bind(TextureQualityPreset::TEXTURE_QUALITY_HIGH), CONNECT_DEFERRED); + presets_hb->add_child(textures_high); + + textures_very_high = memnew(Button); + textures_very_high->set_text(TTRC("Very High")); + textures_very_high->set_theme_type_variation(SceneStringName(FlatButton)); + textures_very_high->set_h_size_flags(SIZE_EXPAND_FILL); + textures_very_high->set_toggle_mode(true); + textures_very_high->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_preset_pressed).bind(TextureQualityPreset::TEXTURE_QUALITY_VERY_HIGH), CONNECT_DEFERRED); + presets_hb->add_child(textures_very_high); + + textures_max = memnew(Button); + textures_max->set_text(TTRC("Max")); + textures_max->set_theme_type_variation(SceneStringName(FlatButton)); + textures_max->set_h_size_flags(SIZE_EXPAND_FILL); + textures_max->set_toggle_mode(true); + textures_max->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_preset_pressed).bind(TextureQualityPreset::TEXTURE_QUALITY_MAX), CONNECT_DEFERRED); + presets_hb->add_child(textures_max); + + textures_vb->add_margin_child(TTRC("Quality Presets"), presets_hb); + + textures_max_lod_slider = memnew(EditorSpinSlider); + textures_max_lod_slider->set_h_size_flags(SIZE_EXPAND_FILL); + textures_max_lod_slider->set_min(0); + textures_max_lod_slider->set_max(13); + textures_max_lod_slider->set_step(1); + textures_max_lod_slider->set_label(TTRC("Max LOD")); + textures_max_lod_slider->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_textures_max_lod_changed), CONNECT_DEFERRED); + textures_vb->add_child(textures_max_lod_slider); + + textures_min_lod_slider = memnew(EditorSpinSlider); + textures_min_lod_slider->set_h_size_flags(SIZE_EXPAND_FILL); + textures_min_lod_slider->set_min(0); + textures_min_lod_slider->set_max(13); + textures_min_lod_slider->set_step(1); + textures_min_lod_slider->set_label(TTRC("Min LOD")); + textures_min_lod_slider->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_textures_min_lod_changed), CONNECT_DEFERRED); + textures_vb->add_child(textures_min_lod_slider); + + VBoxContainer *textures_budget_vb = memnew(VBoxContainer); + textures_budget_vb->set_h_size_flags(SIZE_EXPAND_FILL); + + textures_budget_enable = memnew(CheckBox); + textures_budget_enable->set_text(TTRC("Limit Maximum Texture Memory Budget")); + textures_budget_enable->set_h_size_flags(SIZE_EXPAND_FILL); + textures_budget_enable->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_textures_budget_toggled), CONNECT_DEFERRED); + textures_budget_vb->add_child(textures_budget_enable); + + textures_budget_slider = memnew(EditorSpinSlider); + textures_budget_slider->set_h_size_flags(SIZE_EXPAND_FILL); + textures_budget_slider->set_min(1); + textures_budget_slider->set_max(8192); // 8 GB + textures_budget_slider->set_suffix(" MiB"); + textures_budget_slider->set_step(1); + textures_budget_slider->set_label(TTRC("Memory Budget:")); + textures_budget_slider->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_textures_budget_changed), CONNECT_DEFERRED); + textures_budget_vb->add_child(textures_budget_slider); + + textures_vb->add_margin_child(TTRC("Memory Budget:"), textures_budget_vb); + + Button *textures_save = memnew(Button); + textures_save->set_text(TTRC("Save to Project Settings")); + textures_save->set_anchors_preset(LayoutPreset::PRESET_CENTER_RIGHT); + textures_save->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_textures_save_pressed), CONNECT_DEFERRED); + textures_vb->add_child(textures_save); + + _textures_button_update_state(); + _textures_load_settings(); + _textures_apply_settings(); + } +#endif clear(); // Make sure values are initialized. Will call _snap_update() for us. } Node3DEditor::~Node3DEditor() { diff --git a/editor/scene/3d/node_3d_editor_plugin.h b/editor/scene/3d/node_3d_editor_plugin.h index 7e0bbe6fd922..ab07bdd79b7d 100644 --- a/editor/scene/3d/node_3d_editor_plugin.h +++ b/editor/scene/3d/node_3d_editor_plugin.h @@ -37,14 +37,19 @@ #include "scene/gui/box_container.h" #include "scene/gui/popup.h" +#include "modules/modules_enabled.gen.h" + class AcceptDialog; class Button; +class CheckBox; class ColorPickerButton; class ConfirmationDialog; class DirectionalLight3D; class EditorSelection; class EditorSpinSlider; +class HSlider; class HSplitContainer; +class Label; class LineEdit; class MenuButton; class Node3DEditorViewport; @@ -218,6 +223,10 @@ class Node3DEditor : public VBoxContainer { PopupMenu *gizmos_menu = nullptr; MenuButton *view_layout_menu = nullptr; +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + MenuButton *textures_layout_menu = nullptr; +#endif + AcceptDialog *accept = nullptr; ConfirmationDialog *snap_dialog = nullptr; @@ -348,6 +357,43 @@ class Node3DEditor : public VBoxContainer { Button *sun_environ_settings = nullptr; +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + Button *textures_button = nullptr; + PopupPanel *textures_popup = nullptr; + Button *textures_very_low = nullptr; + Button *textures_low = nullptr; + Button *textures_medium = nullptr; + Button *textures_high = nullptr; + Button *textures_very_high = nullptr; + Button *textures_max = nullptr; + Button *textures_manual = nullptr; + EditorSpinSlider *textures_max_lod_slider = nullptr; + EditorSpinSlider *textures_min_lod_slider = nullptr; + CheckBox *textures_budget_enable = nullptr; + EditorSpinSlider *textures_budget_slider = nullptr; + + enum TextureQualityPreset { + TEXTURE_QUALITY_VERY_LOW, + TEXTURE_QUALITY_LOW, + TEXTURE_QUALITY_MEDIUM, + TEXTURE_QUALITY_HIGH, + TEXTURE_QUALITY_VERY_HIGH, + TEXTURE_QUALITY_MAX, + }; + + void _textures_button_pressed(); + void _textures_button_update_state(); + void _textures_close(const Ref &p_event); + void _textures_preset_pressed(int p_preset); + void _textures_max_lod_changed(float p_value); + void _textures_min_lod_changed(float p_value); + void _textures_budget_toggled(bool p_enabled); + void _textures_budget_changed(float p_value); + void _textures_load_settings(); + void _textures_apply_settings(); + void _textures_save_pressed(); +#endif + DirectionalLight3D *preview_sun = nullptr; bool preview_sun_dangling = false; WorldEnvironment *preview_environment = nullptr; diff --git a/editor/scene/texture/texture_editor_plugin.cpp b/editor/scene/texture/texture_editor_plugin.cpp index 9b2d80af9897..97c7081dd861 100644 --- a/editor/scene/texture/texture_editor_plugin.cpp +++ b/editor/scene/texture/texture_editor_plugin.cpp @@ -47,6 +47,7 @@ #include "scene/resources/image_texture.h" #include "scene/resources/material.h" #include "scene/resources/portable_compressed_texture.h" +#include "scene/resources/streamed_texture.h" #include "scene/resources/texture_rd.h" #include "servers/rendering/rendering_device.h" @@ -351,7 +352,8 @@ bool EditorInspectorPluginTexture::can_handle(Object *p_object) { Object::cast_to(p_object) != nullptr || Object::cast_to(p_object) != nullptr || Object::cast_to(p_object) != nullptr || - Object::cast_to(p_object) != nullptr) { + Object::cast_to(p_object) != nullptr || + Object::cast_to(p_object) != nullptr) { return true; } @@ -365,7 +367,17 @@ bool EditorInspectorPluginTexture::can_handle(Object *p_object) { void EditorInspectorPluginTexture::parse_begin(Object *p_object) { Ref texture(Object::cast_to(p_object)); - if (texture.is_null()) { + if (texture.is_valid()) { + // Load the full-resolution image for streamed textures. + const Ref streamed_texture(texture); + if (streamed_texture.is_valid()) { + const Ref image = streamed_texture->get_image(); + if (image.is_valid()) { + texture = ImageTexture::create_from_image(image); + } + } + } else { + // Not a texture, try to load as an image. Ref image(Object::cast_to(p_object)); texture = ImageTexture::create_from_image(image); diff --git a/main/performance.cpp b/main/performance.cpp index cb4cac0d698e..22e67488559e 100644 --- a/main/performance.cpp +++ b/main/performance.cpp @@ -56,6 +56,12 @@ #include "servers/physics_3d/physics_server_3d.h" #endif // PHYSICS_3D_DISABLED +#include "modules/modules_enabled.gen.h" + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED +#include "modules/texture_streaming/texture_streaming.h" +#endif + Performance *Performance::singleton = nullptr; void Performance::_bind_methods() { @@ -135,6 +141,9 @@ void Performance::_bind_methods() { BIND_ENUM_CONSTANT(NAVIGATION_3D_EDGE_FREE_COUNT); BIND_ENUM_CONSTANT(NAVIGATION_3D_OBSTACLE_COUNT); #endif // NAVIGATION_3D_DISABLED +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + BIND_ENUM_CONSTANT(RENDER_STREAMING_TEXTURE_MEM_USED); +#endif BIND_ENUM_CONSTANT(MONITOR_MAX); BIND_ENUM_CONSTANT(MONITOR_TYPE_QUANTITY); @@ -230,6 +239,9 @@ String Performance::get_monitor_name(Monitor p_monitor) const { PNAME("navigation_3d/edges_free"), PNAME("navigation_3d/obstacles"), #endif // NAVIGATION_3D_DISABLED +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + PNAME("video/streaming_texture_mem_used"), +#endif }; static_assert(std_size(names) == MONITOR_MAX); @@ -454,7 +466,10 @@ double Performance::get_monitor(Monitor p_monitor) const { case NAVIGATION_3D_OBSTACLE_COUNT: return NavigationServer3D::get_singleton()->get_process_info(NavigationServer3D::INFO_OBSTACLE_COUNT); #endif // NAVIGATION_3D_DISABLED - +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + case RENDER_STREAMING_TEXTURE_MEM_USED: + return TextureStreaming::get_singleton()->get_memory_budget_bytes_used(); +#endif // MODULE_TEXTURE_STREAMING_ENABLED default: { } } @@ -527,7 +542,9 @@ Performance::MonitorType Performance::get_monitor_type(Monitor p_monitor) const MONITOR_TYPE_QUANTITY, MONITOR_TYPE_QUANTITY, #endif // _3D_DISABLED - +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + MONITOR_TYPE_MEMORY, +#endif // MODULE_TEXTURE_STREAMING_ENABLED }; static_assert((sizeof(types) / sizeof(MonitorType)) == MONITOR_MAX); diff --git a/main/performance.h b/main/performance.h index 989ec74586ce..b7090da5d25f 100644 --- a/main/performance.h +++ b/main/performance.h @@ -34,6 +34,8 @@ #include "core/templates/hash_map.h" #include "core/variant/type_info.h" +#include "modules/modules_enabled.gen.h" + #define PERF_WARN_OFFLINE_FUNCTION #define PERF_WARN_PROCESS_SYNC @@ -122,6 +124,9 @@ class Performance : public Object { NAVIGATION_3D_EDGE_FREE_COUNT, NAVIGATION_3D_OBSTACLE_COUNT, #endif // _3D_DISABLED +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + RENDER_STREAMING_TEXTURE_MEM_USED, +#endif MONITOR_MAX }; diff --git a/misc/extension_api_validation/4.1-stable_4.2-stable/GH-113429.txt b/misc/extension_api_validation/4.1-stable_4.2-stable/GH-113429.txt new file mode 100644 index 000000000000..bf46df31ca0b --- /dev/null +++ b/misc/extension_api_validation/4.1-stable_4.2-stable/GH-113429.txt @@ -0,0 +1,8 @@ +GH-113429 +--------- +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_albedo': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_emission': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_normal': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_orm': type changed value in new API, from "Texture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". + +Add StreamedTexture2D to the disallowed list for decals. diff --git a/misc/extension_api_validation/4.3-stable_4.4-stable/GH-113429.txt b/misc/extension_api_validation/4.3-stable_4.4-stable/GH-113429.txt new file mode 100644 index 000000000000..df933ce2537e --- /dev/null +++ b/misc/extension_api_validation/4.3-stable_4.4-stable/GH-113429.txt @@ -0,0 +1,8 @@ +GH-113429 +--------- +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_albedo': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_emission': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_normal': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_orm': type changed value in new API, from "Texture2D" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". + +Add StreamedTexture2D to the disallowed list for decals. diff --git a/misc/extension_api_validation/4.5-stable_4.6-stable/GH-113429.txt b/misc/extension_api_validation/4.5-stable_4.6-stable/GH-113429.txt new file mode 100644 index 000000000000..96a9b860780a --- /dev/null +++ b/misc/extension_api_validation/4.5-stable_4.6-stable/GH-113429.txt @@ -0,0 +1,8 @@ +GH-113429 +--------- +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_albedo': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_normal': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_orm': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_emission': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". + +Add StreamedTexture2D to the disallowed list for decals. diff --git a/misc/extension_api_validation/4.6-stable_4.7-stable/GH-113429.txt b/misc/extension_api_validation/4.6-stable_4.7-stable/GH-113429.txt new file mode 100644 index 000000000000..4cec5695f8fa --- /dev/null +++ b/misc/extension_api_validation/4.6-stable_4.7-stable/GH-113429.txt @@ -0,0 +1,8 @@ +GH-113429 +--------- +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_albedo': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_emission': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_normal': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_orm': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". + +Add StreamedTexture2D to the disallowed list for decals. diff --git a/misc/extension_api_validation/4.7-stable/GH-113429.txt b/misc/extension_api_validation/4.7-stable/GH-113429.txt new file mode 100644 index 000000000000..4cec5695f8fa --- /dev/null +++ b/misc/extension_api_validation/4.7-stable/GH-113429.txt @@ -0,0 +1,8 @@ +GH-113429 +--------- +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_albedo': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_emission': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_normal': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". +Validate extension JSON: Error: Field 'classes/Decal/properties/texture_orm': type changed value in new API, from "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture" to "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D". + +Add StreamedTexture2D to the disallowed list for decals. diff --git a/modules/texture_streaming/SCsub b/modules/texture_streaming/SCsub new file mode 100644 index 000000000000..963cfc7707e1 --- /dev/null +++ b/modules/texture_streaming/SCsub @@ -0,0 +1,6 @@ +#!/usr/bin/env python +from misc.utility.scons_hints import * + +Import("env") + +env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build diff --git a/modules/texture_streaming/config.py b/modules/texture_streaming/config.py new file mode 100644 index 000000000000..d22f9454ed25 --- /dev/null +++ b/modules/texture_streaming/config.py @@ -0,0 +1,6 @@ +def can_build(env, platform): + return True + + +def configure(env): + pass diff --git a/modules/texture_streaming/register_types.cpp b/modules/texture_streaming/register_types.cpp new file mode 100644 index 000000000000..13e3b3bca75e --- /dev/null +++ b/modules/texture_streaming/register_types.cpp @@ -0,0 +1,58 @@ +/**************************************************************************/ +/* register_types.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "register_types.h" + +#include "texture_streaming.h" + +#include "core/config/engine.h" +#include "core/object/class_db.h" + +#include "modules/register_module_types.h" + +static TextureStreaming *_texture_streaming_server = nullptr; + +void initialize_texture_streaming_module(ModuleInitializationLevel p_level) { + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + GDREGISTER_CLASS(TextureStreaming); + + _texture_streaming_server = memnew(TextureStreaming); + GDREGISTER_CLASS(TextureStreaming); + Engine::get_singleton()->add_singleton(Engine::Singleton("TextureStreaming", TextureStreaming::get_singleton())); + } +} + +void uninitialize_texture_streaming_module(ModuleInitializationLevel p_level) { + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + if (_texture_streaming_server) { + memdelete(_texture_streaming_server); + } + } +} diff --git a/modules/texture_streaming/register_types.h b/modules/texture_streaming/register_types.h new file mode 100644 index 000000000000..91e41574f698 --- /dev/null +++ b/modules/texture_streaming/register_types.h @@ -0,0 +1,36 @@ +/**************************************************************************/ +/* register_types.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "modules/register_module_types.h" + +void initialize_texture_streaming_module(ModuleInitializationLevel p_level); +void uninitialize_texture_streaming_module(ModuleInitializationLevel p_level); diff --git a/modules/texture_streaming/texture_streaming.cpp b/modules/texture_streaming/texture_streaming.cpp new file mode 100644 index 000000000000..52f49989d4fa --- /dev/null +++ b/modules/texture_streaming/texture_streaming.cpp @@ -0,0 +1,889 @@ +/**************************************************************************/ +/* texture_streaming.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "texture_streaming.h" + +#include "core/config/project_settings.h" +#include "core/math/math_funcs.h" +#include "core/object/callable_mp.h" +#include "core/object/class_db.h" +#include "core/os/os.h" +#include "core/templates/local_vector.h" +#include "core/templates/sort_array.h" +#include "core/typedefs.h" +#include "core/variant/callable.h" +#include "core/variant/variant.h" +#include "servers/display/display_server.h" +#include "servers/rendering/rendering_device.h" +#include "servers/rendering/rendering_server.h" + +// Helper macros for code outside of the rendering server, but that is +// called by the rendering server. +#ifdef DEBUG_ENABLED +#define ERR_NOT_ON_RENDER_THREAD \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL(rendering_server); \ + ERR_FAIL_COND(!rendering_server->is_on_render_thread()); +#define ERR_NOT_ON_RENDER_THREAD_V(m_ret) \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL_V(rendering_server, m_ret); \ + ERR_FAIL_COND_V(!rendering_server->is_on_render_thread(), m_ret); +#else +#define ERR_NOT_ON_RENDER_THREAD +#define ERR_NOT_ON_RENDER_THREAD_V(m_ret) +#endif + +TextureStreaming *TextureStreaming::singleton = nullptr; + +void TextureStreaming::MaterialFeedbackBuffer::clear() { + ERR_NOT_ON_RENDER_THREAD; + rid_map.clear(); + + RD::get_singleton()->buffer_clear(buffer, 0, buffer_size); +} + +void TextureStreaming::MaterialFeedbackBuffer::resize() { + uint32_t material_info_count = TextureStreaming::get_singleton()->material_info_owner.get_count(); + uint32_t material_info_count_bytes = material_info_count * 4; + if (buffer_size >= material_info_count_bytes && material_info_count_bytes > 0) { + return; + } + + // Free the old buffer before creating a new one to avoid RID leaks. + if (buffer.is_valid()) { + RD::get_singleton()->free_rid(buffer); + } + + buffer_size = Math::nearest_power_of_2_templated(MAX(4096u, material_info_count_bytes)); + buffer = RD::get_singleton()->storage_buffer_create(buffer_size, Vector(), 0, RD::BufferCreationBits::BUFFER_CREATION_AS_STORAGE_BIT); + + // Make the vector match the size of the buffer + rid_map.resize(buffer_size / 4); +} + +TextureStreaming::MaterialFeedbackBuffer::MaterialFeedbackBuffer() {} + +TextureStreaming::MaterialFeedbackBuffer::~MaterialFeedbackBuffer() { + if (buffer.is_valid()) { + RS::get_singleton()->call_on_render_thread(callable_mp(RD::get_singleton(), &RD::free_rid).bind(buffer)); + } + + buffer = RID(); + buffer_size = 0; + rid_map.clear(); +} + +TextureStreaming *TextureStreaming::get_singleton() { + return singleton; +} + +uint64_t TextureStreaming::get_memory_budget_bytes_used() { + return texture_streaming_total_memory.load(std::memory_order_relaxed); +} + +void TextureStreaming::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_min_lod_override"), &TextureStreaming::get_min_lod_override); + ClassDB::bind_method(D_METHOD("set_min_lod_override", "min_lod"), &TextureStreaming::set_min_lod_override); + ADD_PROPERTY(PropertyInfo(Variant::INT, "min_lod_override", PROPERTY_HINT_RANGE, "-1,13,1"), "set_min_lod_override", "get_min_lod_override"); + + ClassDB::bind_method(D_METHOD("get_max_lod_override"), &TextureStreaming::get_max_lod_override); + ClassDB::bind_method(D_METHOD("set_max_lod_override", "max_lod"), &TextureStreaming::set_max_lod_override); + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_lod_override", PROPERTY_HINT_RANGE, "-1,13,1"), "set_max_lod_override", "get_max_lod_override"); + + ClassDB::bind_method(D_METHOD("get_memory_budget_mb_override"), &TextureStreaming::get_memory_budget_mb_override); + ClassDB::bind_method(D_METHOD("set_memory_budget_mb_override", "mb"), &TextureStreaming::set_memory_budget_mb_override); + ADD_PROPERTY(PropertyInfo(Variant::INT, "memory_budget_mb_override", PROPERTY_HINT_RANGE, "0,65535,1"), "set_memory_budget_mb_override", "get_memory_budget_mb_override"); + + ClassDB::bind_method(D_METHOD("get_memory_budget_bytes_used"), &TextureStreaming::get_memory_budget_bytes_used); + + ClassDB::bind_method(D_METHOD("flush_texture_streaming"), &TextureStreaming::flush_texture_streaming); + + ADD_SIGNAL(MethodInfo("flush_completed")); +} + +RID TextureStreaming::texture_configure_streaming(RID p_texture, Image::Format p_format, int p_width, int p_height, int p_min_lod, int p_max_lod, Callable p_reload_callable) { + ERR_FAIL_COND_V(p_texture.is_null(), RID()); + ERR_FAIL_COND_V(p_width == 0, RID()); + ERR_FAIL_COND_V(p_height == 0, RID()); + ERR_FAIL_COND_V(!p_reload_callable.is_valid(), RID()); + + return push_and_wait("_texture_configure_streaming_impl", this, &TextureStreaming::_texture_configure_streaming_impl, + p_texture, p_format, p_width, p_height, p_min_lod, p_max_lod, p_reload_callable); +} + +void TextureStreaming::_texture_configure_streaming_impl(Completion *p_completion, RID p_texture, Image::Format p_format, int p_width, int p_height, int p_min_lod, int p_max_lod, const Callable &p_reload_callable) { + print_verbose(vformat("TextureStreaming: Configuring streaming for texture RID(%d) size(%dx%d) format(%d) min_lod(%d) max_lod(%d)", + p_texture.get_id(), p_width, p_height, int(p_format), p_min_lod, p_max_lod)); + + RID rid = streaming_info_owner.allocate_rid(); + StreamingState state; + state.texture = p_texture; + state.format = p_format; + state.width = p_width; + state.height = p_height; + state.min_lod_override = uint8_t(CLAMP(p_min_lod, int(LOD_NO_OVERRIDE), int(MAX_LOD_OVERRIDE))); + state.max_lod_override = uint8_t(CLAMP(p_max_lod, int(LOD_NO_OVERRIDE), int(MAX_LOD_OVERRIDE))); + state.reload_callable = p_reload_callable; + state.current_mip.store(INVALID_MIP, std::memory_order_relaxed); + state.pending_reload_mip.store(INVALID_MIP, std::memory_order_relaxed); + state.requested_tick_msec = 0; + + streaming_info_owner.initialize_rid(rid, state); + + RS::get_singleton()->texture_2d_attach_streaming_state(p_texture, rid); + + p_completion->complete(rid); +} + +void TextureStreaming::texture_remove(RID p_rid) { + push_and_sync("_texture_remove_impl", this, &TextureStreaming::_texture_remove_impl, p_rid); +} + +void TextureStreaming::_texture_remove_impl(CompletionVoid *p_completion, RID p_rid) { + StreamingState *state = streaming_info_owner.get_or_null(p_rid); + if (state) { + state->removing.store(true, std::memory_order_relaxed); + state->pending_reload_mip.exchange(INVALID_MIP, std::memory_order_relaxed); + RS::get_singleton()->texture_2d_attach_streaming_state(state->texture, RID()); + + if (texture_reload_thread.is_started()) { + // Finalize removal on the I/O thread after any earlier reload jobs have drained. + io_command_queue.push_internal("_texture_remove_finalize", this, &TextureStreaming::_texture_remove_finalize, p_rid); + } else { + streaming_info_owner.free(p_rid); + } + } + p_completion->complete(); +} + +void TextureStreaming::texture_update(RID p_rid, int p_width, int p_height, int p_min_lod, int p_max_lod) { + push_and_sync("_texture_update_impl", this, &TextureStreaming::_texture_update_impl, p_rid, p_width, p_height, p_min_lod, p_max_lod); +} + +void TextureStreaming::_texture_update_impl(CompletionVoid *p_completion, RID p_rid, int p_width, int p_height, int p_min_lod, int p_max_lod) { + StreamingState *state = streaming_info_owner.get_or_null(p_rid); + if (state) { + state->width = p_width; + state->height = p_height; + state->min_lod_override = uint8_t(CLAMP(p_min_lod, int(LOD_NO_OVERRIDE), int(MAX_LOD_OVERRIDE))); + state->max_lod_override = uint8_t(CLAMP(p_max_lod, int(LOD_NO_OVERRIDE), int(MAX_LOD_OVERRIDE))); + } + p_completion->complete(); +} + +RID TextureStreaming::material_set_textures(RID p_feedback_rid, const Vector &p_textures) { + ERR_NOT_ON_RENDER_THREAD_V(RID()); + MutexLock lock(material_mutex); + + MaterialInfo *info = nullptr; + if (p_feedback_rid.is_null()) { + info = material_info_owner.allocate(p_feedback_rid); + } else { + info = material_info_owner.get_or_null(p_feedback_rid); + } + + ERR_FAIL_NULL_V(info, RID()); + + if (p_textures.is_empty()) { + material_info_owner.free(p_feedback_rid); + return RID(); + } + + info->textures = p_textures; + + return p_feedback_rid; +} + +TextureStreaming::TextureStreaming() { + singleton = this; + + // Global settings + GLOBAL_DEF_RST("rendering/textures/streaming/enabled", false); + GLOBAL_DEF(PropertyInfo(Variant::BOOL, "rendering/textures/streaming/memory_budget_enabled"), false); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/textures/streaming/memory_budget_mb"), 512); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/textures/streaming/max_ops_per_second"), 200); + + // Mip level settings: 0 = full resolution (best quality), higher = lower resolution + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/textures/streaming/min_lod", PROPERTY_HINT_RANGE, "0,13,1"), 0); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/textures/streaming/max_lod", PROPERTY_HINT_RANGE, "0,13,1"), 3); + + // Inactivity decay: time per mip level of decay in milliseconds. + // The first decay happens after one rate period. Set to 0 to disable decay. + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/textures/streaming/inactivity_decay_rate_ms", PROPERTY_HINT_RANGE, "0,60000,100,or_greater"), 5000); + + // Load initial settings. + setting_streaming_is_enabled = GLOBAL_GET("rendering/textures/streaming/enabled"); + setting_max_ops_per_second = GLOBAL_GET("rendering/textures/streaming/max_ops_per_second"); + setting_min_lod = uint8_t(CLAMP(int(GLOBAL_GET("rendering/textures/streaming/min_lod")), int(MIN_LOD_LEVEL), int(MAX_LOD_LEVEL))); + setting_max_lod = uint8_t(CLAMP(int(GLOBAL_GET("rendering/textures/streaming/max_lod")), int(MIN_LOD_LEVEL), int(MAX_LOD_LEVEL))); + setting_budget_mb = GLOBAL_GET("rendering/textures/streaming/memory_budget_mb"); + setting_budget_enabled = GLOBAL_GET("rendering/textures/streaming/memory_budget_enabled"); + setting_inactivity_decay_rate_ms = GLOBAL_GET("rendering/textures/streaming/inactivity_decay_rate_ms"); + + ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &TextureStreaming::_on_settings_changed)); + + print_verbose(vformat("TextureStreaming settings: enabled=%d, budget_mb=%d, min_lod=%d, max_lod=%d", + (int)setting_streaming_is_enabled, + (int)setting_budget_mb, + setting_min_lod, + setting_max_lod)); + + if (setting_streaming_is_enabled) { + _start_streaming(); + } +} + +TextureStreaming::~TextureStreaming() { + _stop_streaming(); + singleton = nullptr; +} + +void TextureStreaming::_on_settings_changed() { + setting_max_ops_per_second = GLOBAL_GET("rendering/textures/streaming/max_ops_per_second"); + setting_min_lod = uint8_t(CLAMP(int(GLOBAL_GET("rendering/textures/streaming/min_lod")), int(MIN_LOD_LEVEL), int(MAX_LOD_LEVEL))); + setting_max_lod = uint8_t(CLAMP(int(GLOBAL_GET("rendering/textures/streaming/max_lod")), int(MIN_LOD_LEVEL), int(MAX_LOD_LEVEL))); + setting_budget_mb = GLOBAL_GET("rendering/textures/streaming/memory_budget_mb"); + setting_budget_enabled = GLOBAL_GET("rendering/textures/streaming/memory_budget_enabled"); + setting_inactivity_decay_rate_ms = GLOBAL_GET("rendering/textures/streaming/inactivity_decay_rate_ms"); +} + +void TextureStreaming::_start_streaming() { + if (feedback_buffer_thread.is_started()) { + return; // Already running. + } + + const String rendering_method = OS::get_singleton()->get_current_rendering_method(); + if (rendering_method == "gl_compatibility") { + WARN_PRINT("Texture streaming is not supported with the Compatibility renderer."); + return; + } + + feedback_buffer_thread.start(_feedback_buffer_thread_func, this); + texture_reload_thread.start(_texture_reload_thread_func, this); + + // Use frame_post_draw for continuous per-frame feedback submission. + Error err = RenderingServer::get_singleton()->connect("frame_post_draw", callable_mp(this, &TextureStreaming::_feedback_frame_done_callback)); + if (err != OK) { + ERR_PRINT("Failed to connect frame post draw signal."); + } + + // RD initialization must happen on the render thread. + const String display_server_name = DisplayServer::get_singleton()->get_name(); + if (display_server_name != "headless") { + RenderingServer::get_singleton()->call_on_render_thread(callable_mp(this, &TextureStreaming::_render_thread_specific_initialization)); + } +} + +void TextureStreaming::_stop_streaming() { + if (!feedback_buffer_thread.is_started()) { + return; // Not running. + } + + // Prevent the render thread callback from doing further work. + initialized.store(false, std::memory_order_relaxed); + + // Disconnect the frame signal so no new feedback is submitted. + RenderingServer *rs = RenderingServer::get_singleton(); + if (rs && rs->is_connected("frame_post_draw", callable_mp(this, &TextureStreaming::_feedback_frame_done_callback))) { + rs->disconnect("frame_post_draw", callable_mp(this, &TextureStreaming::_feedback_frame_done_callback)); + } + + // Stop the feedback processing thread first — no more budget fitting or mip decisions. + command_queue.request_exit(); + io_command_queue.request_exit(); + + feedback_buffer_thread.wait_to_finish(); + texture_reload_thread.wait_to_finish(); + + // Clean up feedback buffer pool. The MaterialFeedbackBuffer destructor + // schedules RD resource cleanup on the render thread via call_on_render_thread. + { + MutexLock lock(buffer_pool_mutex); + + if (current_feedback_buffer.is_valid()) { + buffer_pool.push_back(current_feedback_buffer); + current_feedback_buffer = RID(); + } + + LocalVector buffers = feedback_buffer_owner.get_owned_list(); + for (RID buffer : buffers) { + feedback_buffer_owner.free(buffer); + } + + buffer_pool.clear(); + } + + print_verbose("TextureStreaming: Streaming stopped."); +} + +void TextureStreaming::_render_thread_specific_initialization() { + ERR_NOT_ON_RENDER_THREAD; + + // Initialize buffer pool + { + MutexLock lock(buffer_pool_mutex); + while (buffer_pool.size() < 3) { + RID buffer = feedback_buffer_owner.allocate_rid(); + MaterialFeedbackBuffer materialFeedbackBuffer; + materialFeedbackBuffer.buffer_size = 0; + materialFeedbackBuffer.self = buffer; + feedback_buffer_owner.initialize_rid(buffer, materialFeedbackBuffer); + buffer_pool.push_back(buffer); + + MaterialFeedbackBuffer *mb = feedback_buffer_owner.get_or_null(buffer); + mb->resize(); + mb->clear(); + + RD::get_singleton()->set_resource_name(mb->buffer, "MaterialFeedbackBuffer_" + itos(buffer.get_id())); + } + } + + // Get an initial buffer. + current_feedback_buffer = _feedback_buffer_get_next(); + + initialized.store(true, std::memory_order_relaxed); +} + +void TextureStreaming::_feedback_frame_done_callback() { + // _feedback_frame_done_callback_render_thread must be called on the render thread since it interacts with RD. + RS::get_singleton()->call_on_render_thread(callable_mp(this, &TextureStreaming::_feedback_frame_done_callback_render_thread)); +} + +void TextureStreaming::_feedback_frame_done_callback_render_thread() { + if (!initialized.load(std::memory_order_relaxed)) { + return; + } + + // Throttle feedback buffer submission to once every 30ms + const uint64_t current_ticks = OS::get_singleton()->get_ticks_msec(); + if (current_ticks - feedback_buffer_last_submit_ticks < 30) { + return; + } + + if (current_feedback_buffer.is_null()) { + WARN_PRINT("TextureStreaming _feedback_frame_done_callback called with invalid feedback buffer RID."); + return; + } + + // Try to get a new buffer for the next frame + RID next_buffer = _feedback_buffer_get_next(); + if (next_buffer.is_null()) { + print_verbose("TextureStreaming _feedback_frame_done_callback called but no feedback buffers are available."); + feedback_buffer_last_submit_ticks = current_ticks; // + return; + } + + // Submit the current feedback buffer + MaterialFeedbackBuffer *mb = feedback_buffer_owner.get_or_null(current_feedback_buffer); + RD::get_singleton()->buffer_get_data_async(mb->buffer, callable_mp(this, &TextureStreaming::_feedback_handle_data).bind(current_feedback_buffer)); + feedback_buffer_last_submit_ticks = current_ticks; + + // Swap to the next buffer + current_feedback_buffer = next_buffer; +} + +RID TextureStreaming::feedback_buffer_get_uniform_rid() { + ERR_NOT_ON_RENDER_THREAD_V(RID()); + MaterialFeedbackBuffer *_buffer = feedback_buffer_owner.get_or_null(current_feedback_buffer); + + if (!initialized.load(std::memory_order_relaxed)) { + // WARN_PRINT("TextureStreaming feedback_buffer_get_uniform_rid: TextureStreaming not initialized yet."); + return RID(); + } + + if (_buffer && _buffer->buffer.is_valid() && _buffer->buffer_size > 0) { + return _buffer->buffer; + } + + if (setting_streaming_is_enabled) { + WARN_PRINT("TextureStreaming feedback_buffer_get_uniform_rid: No valid feedback buffer available, but streaming is enabled."); + } else { + // Streaming is disabled, so this is expected. + } + + return RID(); +} + +RID TextureStreaming::_feedback_buffer_get_next() { + ERR_NOT_ON_RENDER_THREAD_V(RID()); + MutexLock lock(buffer_pool_mutex); + + RID buffer; + if (buffer_pool.size() > 0) { + // Use a buffer from the pool + buffer = buffer_pool[buffer_pool.size() - 1]; + buffer_pool.remove_at(buffer_pool.size() - 1); + } + + if (buffer.is_valid()) { + MaterialFeedbackBuffer *mb = feedback_buffer_owner.get_or_null(buffer); + ERR_FAIL_NULL_V(mb, RID()); + mb->resize(); + mb->clear(); + } + + return buffer; +} + +uint32_t TextureStreaming::feedback_buffer_material_index(RID p_material) { + ERR_NOT_ON_RENDER_THREAD_V(UINT32_MAX); + ERR_FAIL_COND_V(p_material.is_null(), UINT32_MAX); + + // Put the RID for the material into the feedback buffer's rid_map at the index of the material info. + MaterialFeedbackBuffer *_buffer = feedback_buffer_owner.get_or_null(current_feedback_buffer); + if (_buffer) { + // material_info_owner.get_index() is thread-safe (only reads from RID_Owner which is thread-safe) + uint32_t index = material_info_owner.get_index(p_material); + if (_buffer->rid_map.size() <= (index + 1)) { + _buffer->rid_map.resize(index + 1); + } + + _buffer->rid_map[index] = p_material; + + return index; + } + + return UINT32_MAX; +} + +void TextureStreaming::_feedback_handle_data(const PackedByteArray &p_array, RID p_buffer) { + if (TextureStreaming::get_singleton() == nullptr) { + return; + } + ERR_FAIL_COND(!p_buffer.is_valid()); + MaterialFeedbackBuffer *mb = feedback_buffer_owner.get_or_null(p_buffer); + if (mb == nullptr) { + return; + } + + mb->data = p_array; + + uint64_t current_ticks = OS::get_singleton()->get_ticks_msec(); + command_queue.push_internal("_process_material_feedback_buffer", this, &TextureStreaming::_process_material_feedback_buffer, mb, current_ticks); +} + +void TextureStreaming::_process_material_feedback_buffer(MaterialFeedbackBuffer *p_mb, uint64_t p_ticks_msec) { + uint32_t *data_ptr = (uint32_t *)p_mb->data.ptrw(); + ERR_FAIL_NULL(data_ptr); + + // Distribute feedback to materials and their associated textures + const uint32_t *p_data = (uint32_t *)data_ptr; + { + MutexLock lock(material_mutex); + for (uint32_t i = 0; i < p_mb->rid_map.size(); i++) { + RID material_rid = p_mb->rid_map[i]; + uint32_t material_feedback = p_data[i]; + + if (material_feedback == 0) { + continue; + } + + // Undo the bitwise NOT applied in the shader (reverse of ~floatBitsToUint in shader) + uint32_t original_bits = ~material_feedback; + float min_uv_sq; + memcpy(&min_uv_sq, &original_bits, sizeof(float)); + min_uv_sq = Math::is_nan(min_uv_sq) ? 1.0f : min_uv_sq; + float min_uv_len = Math::sqrt(min_uv_sq); + MaterialInfo *info = material_info_owner.get_or_null(material_rid); + if (!info) { + continue; + } + + const Vector &textures = info->textures; + for (RID texture_rid : textures) { + StreamingState *state = streaming_info_owner.get_or_null(texture_rid); + + // If the texture has been deleted but the material hasn't updated the list of textures yet, skip it. + if (!state || state->removing.load(std::memory_order_relaxed)) { + continue; + } + + // Determine required mip level based on smoothed UV length and texture size. + // For rectangular textures, use the smaller dimension to avoid undersampling. + float min_dim = MIN(float(state->width), float(state->height)); + float texel_coverage = min_uv_len * min_dim; + + // Guard against invalid values that would cause undefined behavior in log2. + // If texel_coverage is <= 0, request highest quality (mip 0). + int required_mip = (texel_coverage > 0.0f) ? int(Math::floor(Math::log2(texel_coverage))) : 0; + + // Clamp to valid mip range for this texture. + uint8_t clamped_mip = uint8_t(CLAMP(required_mip, 0, int(state->get_mip_count()) - 1)); + + // Update feedback if this is a better (lower) mip level than previously recorded + // Lower mip = higher quality needed + if (clamped_mip < state->feedback_mip) { + state->requested_tick_msec = p_ticks_msec; + state->feedback_mip = clamped_mip; + } + } + } + } + + { + // Return buffer to the pool since we're done with it. + MutexLock lock(buffer_pool_mutex); + buffer_pool.push_back(p_mb->self); + } + + { + // Run fit/process algorithm + _feedback_buffer_process(p_ticks_msec); + } +} + +void TextureStreaming::_feedback_buffer_thread_func(void *p_udata) { + Thread::set_name("TexStreaming"); + + TextureStreaming *texture_streaming = static_cast(p_udata); + + print_verbose("Texture Streaming process thread starting..."); + + texture_streaming->_feedback_buffer_thread_main(); +} + +void TextureStreaming::_feedback_buffer_thread_main() { + // Main loop: wait for commands and process them + // wait_and_flush() returns false when exit is requested + while (command_queue.wait_and_flush()) { + // Commands are already flushed by wait_and_flush() + // Add any per-iteration work here if needed + } +} + +// Fits requested mip levels to the streaming budget. +// +// Phase 1 builds candidates from shader feedback and cached state. +// Phase 2 raises the least important candidates by one mip at a time until the budget fits. +// Phase 3 stores fit_mip and queues reloads toward it. +void TextureStreaming::_feedback_buffer_process(uint64_t p_ticks_msec) { + const LocalVector buffers = streaming_info_owner.get_owned_list(); + + // Reuse vector to reduce allocations. + fit_candidates.clear(); + fit_candidates.reserve(buffers.size()); + + // Phase 1: Build candidate list with initial target mip levels. + uint64_t total_requested_bytes = 0; + for (uint32_t i = 0; i < buffers.size(); i++) { + StreamingState *state = streaming_info_owner.get_or_null(buffers[i]); + if (!state || state->removing.load(std::memory_order_relaxed)) { + continue; + } + + // Compute effective best/worst mip (0 = highest quality). + // Per-texture overrides are hard bounds. System overrides/project settings + // are intersected with those bounds rather than replacing them. + uint8_t system_min_lod = _get_system_min_lod(); + uint8_t system_max_lod = _get_system_max_lod(); + uint8_t texture_min_lod = state->min_lod_override != LOD_NO_OVERRIDE ? state->min_lod_override - 1 : MIN_LOD_LEVEL; + uint8_t texture_max_lod = state->max_lod_override != LOD_NO_OVERRIDE ? state->max_lod_override - 1 : MAX_LOD_LEVEL; + + // Ensure weird settings where max is larger than min do something more sane. + system_min_lod = MIN(system_min_lod, system_max_lod); + system_max_lod = MAX(system_min_lod, system_max_lod); + texture_min_lod = MIN(texture_min_lod, texture_max_lod); + texture_max_lod = MAX(texture_min_lod, texture_max_lod); + + uint8_t selected_min_lod = MAX(system_min_lod, texture_min_lod); + uint8_t selected_max_lod = MIN(system_max_lod, texture_max_lod); + if (selected_min_lod > selected_max_lod) { + if (system_min_lod >= texture_max_lod) { // Fixes the boundary leak + selected_min_lod = texture_max_lod; + selected_max_lod = texture_max_lod; + } else { + selected_min_lod = texture_min_lod; + selected_max_lod = texture_min_lod; + } + } + + uint8_t raw_request_mip = state->feedback_mip; + state->request_mip = state->update(raw_request_mip, p_ticks_msec, 0.00005f); + uint8_t desired_mip = CLAMP(state->request_mip, selected_min_lod, selected_max_lod); + + // Only allow improvements freely; reductions happen via budget fitting or decay. + uint8_t initial_target; + uint8_t cached_fit_mip = state->fit_mip.load(std::memory_order_relaxed); + uint8_t cached_current_mip = state->current_mip.load(std::memory_order_relaxed); + if (cached_fit_mip < INVALID_MIP && cached_fit_mip >= selected_min_lod && cached_fit_mip <= selected_max_lod) { + initial_target = MIN(desired_mip, cached_fit_mip); + } else if (cached_current_mip < INVALID_MIP) { + initial_target = MIN(desired_mip, cached_current_mip); + } else { + initial_target = desired_mip; + } + + // Apply inactivity decay. + const uint64_t msecs_since_request = (state->requested_tick_msec > 0) ? (p_ticks_msec - state->requested_tick_msec) : 0; + if (setting_inactivity_decay_rate_ms > 0) { + const uint32_t decay_mips = uint32_t(msecs_since_request / setting_inactivity_decay_rate_ms); + if (decay_mips > 0) { + // Increase target mip (reduce quality) by decay amount, clamped to max_lod + initial_target = uint8_t(MIN(uint32_t(initial_target) + decay_mips, uint32_t(selected_max_lod))); + } + } + + initial_target = CLAMP(initial_target, selected_min_lod, selected_max_lod); + + FitCandidate candidate; + candidate.state_rid = buffers[i]; + candidate.state = state; + candidate.min_lod = selected_min_lod; + candidate.max_lod = selected_max_lod; + candidate.target_mip = initial_target; + candidate.inactivity_msec = msecs_since_request; + + candidate.bytes = state->get_bytes_at_mip(initial_target); + total_requested_bytes += candidate.bytes; + fit_candidates.push_back(candidate); + + state->feedback_mip = INVALID_MIP; + } + + // Phase 2: Budget fitting - iteratively reduce textures until within memory budget. + uint64_t assigned_bytes = total_requested_bytes; + const uint64_t budget_bytes = setting_budget_enabled ? uint64_t(_get_memory_budget_mb()) * 1024ull * 1024ull : 0; + if (budget_bytes > 0) { + // Symmetric hysteresis with a 10% total deadband. + const uint64_t budget_high = budget_bytes + (budget_bytes / 20); // +5% = trigger reduction + const uint64_t budget_low = budget_bytes - (budget_bytes / 20); // -5% = stop reduction + + if (budget_bytes > 0 && assigned_bytes > budget_high) { + // Build heap of reducible candidates. + reduction_heap.clear(); + reduction_heap.reserve(fit_candidates.size()); + for (uint32_t i = 0; i < fit_candidates.size(); i++) { + if (fit_candidates[i].state && fit_candidates[i].target_mip < fit_candidates[i].max_lod) { + reduction_heap.push_back(&fit_candidates[i]); + } + } + + // Create max-heap (least important texture at top). + SortArray heap_sorter{}; + heap_sorter.make_heap(0, reduction_heap.size(), reduction_heap.ptr()); + + int64_t heap_size = reduction_heap.size(); + while (assigned_bytes > budget_low && heap_size > 0) { + heap_sorter.pop_heap(0, heap_size, reduction_heap.ptr()); + heap_size--; + FitCandidate *best = reduction_heap[heap_size]; + + if (!best->state || best->target_mip >= best->max_lod) { + continue; + } + + // Reduce texture by one mip level. + uint8_t previous_mip = best->target_mip; + uint8_t increased_mip = MIN(best->max_lod, uint8_t(previous_mip + 1)); + if (increased_mip == previous_mip) { + // Already at minimum quality - mark exhausted and continue + best->state = nullptr; + continue; + } + + uint64_t previous_bytes = best->bytes; + uint64_t new_bytes = best->state->get_bytes_at_mip(increased_mip); + best->target_mip = increased_mip; + best->bytes = new_bytes; + + ERR_FAIL_COND(new_bytes > previous_bytes); + assigned_bytes -= (previous_bytes - new_bytes); + + // Re-insert candidate if still reducible. + if (best->target_mip < best->max_lod) { + reduction_heap[heap_size] = best; + heap_size++; + heap_sorter.push_heap(0, heap_size - 1, 0, best, reduction_heap.ptr()); + } + } + + if (assigned_bytes > budget_low) { + WARN_PRINT_ONCE(vformat( + "Texture streaming budget cannot be satisfied even at minimum quality. Used %s, budget %s bytes.", String::humanize_size(assigned_bytes), String::humanize_size(budget_bytes))); + } + } + } + + // Phase 3: Apply fitted mip levels and queue reloads on the I/O thread. + uint64_t memory = 0; + for (uint32_t i = 0; i < fit_candidates.size(); i++) { + FitCandidate &candidate = fit_candidates[i]; + StreamingState *state = candidate.state; + if (!state) { + continue; + } + + state->fit_mip.store(candidate.target_mip, std::memory_order_relaxed); + + uint8_t cur = state->current_mip.load(std::memory_order_relaxed); + if (cur == INVALID_MIP) { + state->current_mip.store(candidate.max_lod, std::memory_order_relaxed); + cur = candidate.max_lod; + } + + memory += state->get_bytes_at_mip(cur); + + // Queue a reload if the current mip still differs from fit_mip. + // Use candidate.target_mip directly — this thread is the sole writer of fit_mip. + if (cur != candidate.target_mip) { + uint8_t old_pending = state->pending_reload_mip.exchange(candidate.target_mip, std::memory_order_relaxed); + if (old_pending == INVALID_MIP) { + io_command_queue.push_internal("_do_texture_reload", this, &TextureStreaming::_do_texture_reload, candidate.state_rid); + } + } + } + + texture_streaming_total_memory.store(memory, std::memory_order_relaxed); +} + +void TextureStreaming::_texture_reload_thread_func(void *p_udata) { + Thread::set_name("TexStreaming IO"); + + TextureStreaming *tss = static_cast(p_udata); + + print_verbose("Texture Streaming i/o thread starting..."); + + tss->_texture_reload_thread_main(); +} + +void TextureStreaming::_texture_reload_thread_main() { + // Main loop: wait for commands and process them + // wait_and_flush() returns false when exit is requested + while (io_command_queue.wait_and_flush()) { + // Commands are already flushed by wait_and_flush() + // Add any per-iteration work here if needed + } +} + +void TextureStreaming::_do_texture_reload(RID p_state_rid) { + StreamingState *state = streaming_info_owner.get_or_null(p_state_rid); + if (state == nullptr || state->removing.load(std::memory_order_relaxed)) { + return; + } + + // Read and clear the pending target mip. + uint8_t target_mip = state->pending_reload_mip.exchange(INVALID_MIP, std::memory_order_relaxed); + + if (target_mip == INVALID_MIP) { + // Already processed or cancelled. + return; + } + + uint8_t current = state->current_mip.load(std::memory_order_relaxed); + if (target_mip == current) { + // Already at target, nothing to do. + return; + } + + bool is_flushing = flushing_count.load(std::memory_order_relaxed) > 0; + + // Throttle I/O operations to limit disk/memory bandwidth usage. + // Skip throttling during flush for immediate loading. + if (!is_flushing && setting_max_ops_per_second > 0) { + const uint64_t min_interval_usec = 1000000 / setting_max_ops_per_second; + if (min_interval_usec > 0) { + const uint64_t sleep_threshold_usec = 2000; + next_io_op_ticks += min_interval_usec; + const uint64_t current_ticks = OS::get_singleton()->get_ticks_usec(); + if (next_io_op_ticks > current_ticks) { + const uint64_t ahead = next_io_op_ticks - current_ticks; + if (ahead >= sleep_threshold_usec) { + OS::get_singleton()->delay_usec(ahead); + } + } else { + // reset so credit can't accumulate across I/O stalls. + next_io_op_ticks = current_ticks; + } + } + } + + uint8_t next_mip; + if (is_flushing) { + // During flush, jump directly to target mip level. + next_mip = target_mip; + } else if (target_mip < current) { + next_mip = current - 1; // Improving quality. + } else { + next_mip = current + 1; // Reducing quality. + } + + // Perform the actual texture reload at the new mip level. + state->reload_callable.call(next_mip); + state->current_mip.store(next_mip, std::memory_order_relaxed); + + // If we haven't reached the target yet, requeue to keep stepping. + // Re-read fit_mip in case the processing thread updated it since we started. + uint8_t current_target = state->fit_mip.load(std::memory_order_relaxed); + if (!state->removing.load(std::memory_order_relaxed) && next_mip != current_target) { + uint8_t expected = INVALID_MIP; + if (state->pending_reload_mip.compare_exchange_strong(expected, current_target, std::memory_order_relaxed)) { + io_command_queue.push_internal("_do_texture_reload", this, &TextureStreaming::_do_texture_reload, p_state_rid); + } + } +} + +void TextureStreaming::_texture_remove_finalize(RID p_state_rid) { + StreamingState *state = streaming_info_owner.get_or_null(p_state_rid); + if (state) { + streaming_info_owner.free(p_state_rid); + } +} + +void TextureStreaming::flush_texture_streaming() { + print_verbose("Texture Streaming flush requested."); + if (!feedback_buffer_thread.is_started()) { + // Not running — nothing to flush, signal immediately. + callable_mp(this, &TextureStreaming::_emit_flush_completed).call_deferred(); + return; + } + command_queue.push_internal("_flush_texture_streaming_impl", this, &TextureStreaming::_flush_texture_streaming_impl); +} + +void TextureStreaming::_flush_texture_streaming_impl() { + flushing_count.fetch_add(1, std::memory_order_relaxed); + + // Run a full feedback processing cycle to set fit_mip targets and queue reloads. + uint64_t ticks_msec = OS::get_singleton()->get_ticks_msec(); + _feedback_buffer_process(ticks_msec); + + // Push a fence to the I/O thread. Since flushing_count > 0, all reloads queued above + // will jump directly to their target mip. The fence fires after they all complete. + io_command_queue.push_internal("_flush_fence", this, &TextureStreaming::_flush_fence); +} + +void TextureStreaming::_flush_fence() { + flushing_count.fetch_sub(1, std::memory_order_relaxed); + callable_mp(this, &TextureStreaming::_emit_flush_completed).call_deferred(); +} + +void TextureStreaming::_emit_flush_completed() { + print_verbose("Texture Streaming flush completed."); + emit_signal(SNAME("flush_completed")); +} diff --git a/modules/texture_streaming/texture_streaming.h b/modules/texture_streaming/texture_streaming.h new file mode 100644 index 000000000000..c7f7b2524a37 --- /dev/null +++ b/modules/texture_streaming/texture_streaming.h @@ -0,0 +1,669 @@ +/**************************************************************************/ +/* texture_streaming.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "modules/modules_enabled.gen.h" + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + +#include "core/io/image.h" +#include "core/os/condition_variable.h" +#include "core/os/thread.h" +#include "core/templates/rid_owner.h" +#include "core/templates/tuple.h" + +// TextureStreaming singleton. +// Thread safety notes: +// - Public API methods are thread-safe and can be called from any thread. +// - Internal _impl methods usually run on the feedback processing thread, but may run on the caller thread before streaming starts. +// - Methods with ERR_NOT_ON_RENDER_THREAD must be called from the render thread. +// - I/O operations run on a dedicated texture reload thread. +class TextureStreaming : public Object { + GDCLASS(TextureStreaming, Object); + + static TextureStreaming *singleton; + + // Sentinel value indicating no valid mip level (uninitialized or no feedback) + static constexpr uint8_t INVALID_MIP = 255; + +public: + static constexpr uint8_t MIN_LOD_LEVEL = 0; + static constexpr uint8_t MAX_LOD_LEVEL = 13; + + // Overrides are 1 higher, with zero meaning no override. + static constexpr uint8_t LOD_NO_OVERRIDE = 0; + static constexpr uint8_t MAX_LOD_OVERRIDE = MAX_LOD_LEVEL + 1; + +protected: + static void _bind_methods(); + +private: + // Single-producer, single-consumer command queue used by the feedback and I/O threads. + class SimpleCommandQueue { + static const size_t MAX_COMMAND_SIZE = 1024; + + struct CommandBase { + virtual void call() = 0; + virtual ~CommandBase() = default; + }; + + template + struct Command : public CommandBase { + T *instance; + M method; + Tuple...> args; + + template + _FORCE_INLINE_ Command([[maybe_unused]] const char *p_method_name, T *p_instance, M p_method, FwdArgs &&...p_args) : + instance(p_instance), method(p_method), args(std::forward(p_args)...) { + } + + void call() override { + call_impl(BuildIndexSequence{}); + } + + private: + template + _FORCE_INLINE_ void call_impl(IndexSequence) { + (instance->*method)(std::move(get())...); + } + + template + _FORCE_INLINE_ auto &get() { return ::tuple_get(args); } + }; + + static const uint32_t DEFAULT_COMMAND_MEM_SIZE_KB = 64; + + mutable BinaryMutex mutex; + mutable ConditionVariable condvar; + LocalVector command_mem; + bool pending = false; // Always accessed under `mutex`; no atomic needed. + bool should_exit = false; // Always accessed under `mutex`; no atomic needed. + + template + _FORCE_INLINE_ void create_command([[maybe_unused]] const char *p_method_name, Args &&...p_args) { + constexpr uint64_t alloc_size = ((sizeof(T) + 8U - 1U) & ~(8U - 1U)); + static_assert(alloc_size < UINT32_MAX, "Type too large to fit in the command queue."); + + uint64_t size = command_mem.size(); + command_mem.resize(size + alloc_size + sizeof(uint64_t)); + *(uint64_t *)&command_mem[size] = alloc_size; + void *cmd = &command_mem[size + sizeof(uint64_t)]; + new (cmd) T(p_method_name, std::forward(p_args)...); + } + + void _flush_locked(MutexLock &p_lock) { + if (!pending) { + return; + } + + // Buffer to copy command into before unlocking. + // This protects against reallocs invalidating the pointer during execution. + char cmd_buffer[MAX_COMMAND_SIZE]; + + uint64_t read_ptr = 0; + while (read_ptr < command_mem.size()) { + uint64_t size = *(uint64_t *)&command_mem[read_ptr]; + read_ptr += sizeof(uint64_t); + + CommandBase *cmd = reinterpret_cast(&command_mem[read_ptr]); + + // Copy command to local buffer so we can safely unlock during execution. + // The copy is shallow - complex types like Callable share internal data with the original. + memcpy(cmd_buffer, (char *)cmd, size); + + read_ptr += size; + + // Unlock while executing the command (allows new commands to be pushed) + p_lock.temp_unlock(); + reinterpret_cast(cmd_buffer)->call(); + p_lock.temp_relock(); + + // Destroy the ORIGINAL after execution completes. + // We must not destroy before call() because the backup is a shallow copy + // that shares internal data (e.g., Callable's bound arguments). + // Handle potential realloc that may have happened during unlock. + cmd = reinterpret_cast(&command_mem[read_ptr - size]); + cmd->~CommandBase(); + } + + command_mem.clear(); + pending = false; + } + + public: + // Push a command to the queue and wake the waiting thread. + template + void push_internal(const char *p_method_name, T *p_instance, M p_method, Args &&...p_args) { + using CommandType = Command; + static_assert(sizeof(CommandType) <= MAX_COMMAND_SIZE); + + MutexLock lock(mutex); + create_command(p_method_name, p_instance, p_method, std::forward(p_args)...); + pending = true; + condvar.notify_one(); + } + + // Signal the thread to exit and wake it up. + void request_exit() { + MutexLock lock(mutex); + should_exit = true; + condvar.notify_one(); + } + + // Flush all pending commands. Call from the processing thread. + void flush() { + MutexLock lock(mutex); + _flush_locked(lock); + } + + // Wait for commands or exit signal, then flush. Returns true if should continue, false if should exit. + // This is the main loop function for the processing thread. + bool wait_and_flush() { + MutexLock lock(mutex); + + // Wait until we have work or should exit + while (!pending && !should_exit) { + condvar.wait(lock); + } + + if (should_exit) { + // Flush any remaining commands before exiting + _flush_locked(lock); + return false; + } + + _flush_locked(lock); + return true; + } + + // Reset the queue so it can be reused after request_exit(). + // Must only be called after the consumer thread has finished (wait_to_finish). + void reset() { + MutexLock lock(mutex); + should_exit = false; + pending = false; + command_mem.clear(); + } + + SimpleCommandQueue() { + command_mem.reserve(DEFAULT_COMMAND_MEM_SIZE_KB * 1024); + } + + ~SimpleCommandQueue() { + // Flush any remaining commands + flush(); + } + }; + + // Helper class to manage RID_Owner with index-based access. + // Thread-safe version: protects index_to_rid with a mutex. + template + class RID_IndexedOwner { + static constexpr uint32_t INVALID_INDEX = 0xFFFFFFFF; + + struct Wrapper { + T data; + uint32_t index; + }; + + mutable RID_Owner owner; + mutable BinaryMutex index_mutex; + LocalVector index_to_rid; + + public: + _FORCE_INLINE_ T *get_or_null(const RID &p_rid) { + // No lock needed - RID_Owner is already thread-safe for lookups + Wrapper *wrapper = owner.get_or_null(p_rid); + return wrapper ? &wrapper->data : nullptr; + } + + _FORCE_INLINE_ T *allocate(RID &p_rid) { + MutexLock lock(index_mutex); + uint32_t new_index = index_to_rid.size(); + Wrapper wrapper; + wrapper.index = new_index; + RID rid = owner.allocate_rid(); + owner.initialize_rid(rid, wrapper); + index_to_rid.push_back(rid); + Wrapper *wrapper_ptr = owner.get_or_null(rid); + p_rid = rid; + return wrapper_ptr ? &wrapper_ptr->data : nullptr; + } + + _FORCE_INLINE_ uint32_t get_index(const RID &p_rid) const { + // No lock needed - we only read from owner which is thread-safe + Wrapper *wrapper = owner.get_or_null(p_rid); + return wrapper ? wrapper->index : INVALID_INDEX; + } + + _FORCE_INLINE_ void free(const RID &p_rid) { + MutexLock lock(index_mutex); + Wrapper *wrapper = owner.get_or_null(p_rid); + if (wrapper) { + uint32_t index = wrapper->index; + uint32_t last_index = index_to_rid.size() - 1; + + if (index != last_index) { + // Move last element to the freed slot + RID last_rid = index_to_rid[last_index]; + index_to_rid[index] = last_rid; + + // Update the moved element's index + Wrapper *last_wrapper = owner.get_or_null(last_rid); + if (last_wrapper) { + last_wrapper->index = index; + } + } + + index_to_rid.resize(last_index); + owner.free(p_rid); + } + } + + _FORCE_INLINE_ uint32_t get_count() const { + MutexLock lock(index_mutex); + return index_to_rid.size(); + } + }; + + // Per texture streaming state + // All mip levels use the convention: 0 = full resolution, higher = lower quality + // + // Thread access model: + // feedback_mip, request_mip, smoothed_mip, requested_tick_msec, + // width, height, format, min_lod_override, max_lod_override — feedback thread only. + // fit_mip — written by feedback thread, read by I/O thread. + // current_mip — written by I/O thread (and bootstrap on feedback), read by feedback thread. + // pending_reload_mip — exchanged/CAS'd by both threads to coordinate reload ownership. + // removing — write-once (feedback → I/O), acts as a cancellation flag. + // + // All inter-thread visibility for the above is guaranteed by the command-queue + // mutexes (push → wait_and_flush). The atomics therefore only need atomicity + // (tear-free reads/writes), not ordering, so memory_order_relaxed is used. + struct StreamingState { + // Configuration + RID texture; + uint16_t width = 0; + uint16_t height = 0; + Image::Format format = Image::FORMAT_MAX; + + // Settings & Constraints (mip levels) + Callable reload_callable; + uint8_t min_lod_override = LOD_NO_OVERRIDE; // Best quality mip level allowed + uint8_t max_lod_override = LOD_NO_OVERRIDE; // Worst quality mip level allowed + + // Runtime state (mip levels) + uint8_t feedback_mip = INVALID_MIP; // The mip level requested by shader feedback (INVALID_MIP = not requested) + uint8_t request_mip = INVALID_MIP; // The mip level after clamping to best/worst + std::atomic fit_mip = INVALID_MIP; // The mip level that fits in the budget + std::atomic current_mip = INVALID_MIP; // The current mip level that has been set + std::atomic pending_reload_mip = INVALID_MIP; // Mip level queued for reload (INVALID_MIP = none pending) + std::atomic removing{ false }; // True once removal starts; blocks new reload work and final free waits on the I/O queue. + + // NOTE: All atomic operations on these fields use memory_order_relaxed. + // Ordering is provided by the command-queue mutexes, not by the atomics themselves. + uint64_t requested_tick_msec = 0; + + // Helper to get the number of mip levels for this texture + _FORCE_INLINE_ uint8_t get_mip_count() const { + const uint32_t max_dim = MAX(width, height); + return max_dim > 0 ? uint8_t(Math::floor(Math::log2(float(max_dim)))) + 1 : 1; + } + + // Helper to get memory size at a specific mip level + _FORCE_INLINE_ uint64_t get_bytes_at_mip(uint8_t p_mip) const { + uint32_t w = MAX(1u, uint32_t(width) >> p_mip); + uint32_t h = MAX(1u, uint32_t(height) >> p_mip); + return Image::get_image_data_size(w, h, format, true); + } + + StreamingState() {} + + StreamingState(const StreamingState &p_other) { + texture = p_other.texture; + width = p_other.width; + height = p_other.height; + format = p_other.format; + reload_callable = p_other.reload_callable; + min_lod_override = p_other.min_lod_override; + max_lod_override = p_other.max_lod_override; + feedback_mip = p_other.feedback_mip; + request_mip = p_other.request_mip; + fit_mip = p_other.fit_mip.load(std::memory_order_relaxed); + current_mip = p_other.current_mip.load(std::memory_order_relaxed); + pending_reload_mip = p_other.pending_reload_mip.load(std::memory_order_relaxed); + removing = p_other.removing.load(std::memory_order_relaxed); + requested_tick_msec = p_other.requested_tick_msec; + } + + float smoothed_mip = 15.0f; // Start at max mip (lowest quality) + uint64_t last_update_tick_msec = 0; + + // Tracks the lowest requested mip and lets it drift upward over time to avoid rapid changes from view jitter. + _FORCE_INLINE_ float update(uint32_t p_mip_level, uint64_t p_current_tick_msec, float p_decay_per_msec = 0.0001f) { + float value = float(p_mip_level); + // Linear decay upward based on time (toward higher mip = lower quality) + if (last_update_tick_msec > 0) { + uint64_t delta_msec = p_current_tick_msec - last_update_tick_msec; + smoothed_mip = MIN(15.0f, smoothed_mip + (float(delta_msec) * p_decay_per_msec)); + } + smoothed_mip = MIN(value, smoothed_mip); + last_update_tick_msec = p_current_tick_msec; + + return smoothed_mip; + } + }; + + RID_Owner streaming_info_owner; + + // Rendering feedback buffer and the RID map used to interpret its entries. + struct MaterialFeedbackBuffer { + RID buffer; // RID for the material feedback buffer. + uint32_t buffer_size = 0; // Size of the buffer in bytes. + LocalVector rid_map; // Maps indices in the buffer to texture RIDs. + RID self; + + PackedByteArray data; + + void clear(); + void resize(); + + MaterialFeedbackBuffer(); + ~MaterialFeedbackBuffer(); + }; + RID_Owner feedback_buffer_owner; + + BinaryMutex material_mutex; + + // Buffer pool management + BinaryMutex buffer_pool_mutex; + Vector buffer_pool; + + struct MaterialInfo { + Vector textures; + }; + RID_IndexedOwner material_info_owner; + + // Settings + bool setting_streaming_is_enabled = true; + uint32_t setting_budget_mb = 512; + bool setting_budget_enabled = false; + uint8_t setting_min_lod = MIN_LOD_LEVEL; // Best quality mip level - 0 = full resolution + uint8_t setting_max_lod = 3; // Worst quality mip level - 3 ≈ 128px for 8K texture + uint32_t setting_max_ops_per_second = 500; // 60 fps would be ~8 ops/frame + uint64_t setting_inactivity_decay_rate_ms = 5000; // Time per mip level of decay (ms) + uint32_t setting_budget_mb_override = UINT32_MAX; + uint8_t setting_min_lod_override = UINT8_MAX; + uint8_t setting_max_lod_override = UINT8_MAX; + + void clear_overrides() { + setting_budget_mb_override = UINT32_MAX; + setting_min_lod_override = UINT8_MAX; + setting_max_lod_override = UINT8_MAX; + } + + _FORCE_INLINE_ uint32_t _get_memory_budget_mb() const { return setting_budget_mb_override != UINT32_MAX ? setting_budget_mb_override : setting_budget_mb; } + _FORCE_INLINE_ uint8_t _get_system_min_lod() const { return setting_min_lod_override != UINT8_MAX ? setting_min_lod_override : setting_min_lod; } + _FORCE_INLINE_ uint8_t _get_system_max_lod() const { return setting_max_lod_override != UINT8_MAX ? setting_max_lod_override : setting_max_lod; } + + // Feedback buffer processing + Thread feedback_buffer_thread; + uint64_t feedback_buffer_last_submit_ticks = 0; + + // Budget fitting candidate structure. + // Tracks each texture's constraints, target mip level, and priority metrics. + // Mip levels: 0 = full resolution (best quality), higher = lower quality + struct FitCandidate { + RID state_rid; + StreamingState *state = nullptr; // Pointer to the texture's streaming state (nullptr = exhausted/skip) + uint8_t min_lod = MIN_LOD_LEVEL; // Best quality mip level allowed + uint8_t max_lod = MAX_LOD_LEVEL; // Worst quality mip level allowed + uint8_t target_mip = 0; // Current target mip level (adjusted during budget fitting) + uint64_t inactivity_msec = 0; // Time since last shader request for this texture + uint64_t bytes = 0; // Memory footprint at target_mip (updated as target changes) + }; + LocalVector fit_candidates; // Reused across _feedback_buffer_process calls to reduce allocations + + struct FitCandidateComparator { + _FORCE_INLINE_ bool operator()(const FitCandidate *p_a, const FitCandidate *p_b) const { + // Skip exhausted candidates (treat as lowest priority) + // Exhausted = already at max_lod (lowest quality) + if (!p_a->state || p_a->target_mip >= p_a->max_lod) { + return false; // 'a' is exhausted, so 'b' has priority + } + + if (!p_b->state || p_b->target_mip >= p_b->max_lod) { + return true; // 'b' is exhausted, so 'a' has priority + } + + // Determine if candidates are below their requested mip (higher quality than requested) + // Note: lower mip = higher quality, so target_mip < request_mip means over-quality + uint8_t a_requested = CLAMP(p_a->state->request_mip, p_a->min_lod, p_a->max_lod); + uint8_t b_requested = CLAMP(p_b->state->request_mip, p_b->min_lod, p_b->max_lod); + bool a_over_quality = p_a->target_mip < a_requested; + bool b_over_quality = p_b->target_mip < b_requested; + + // Priority 1: Prefer reducing textures that are above their requested quality + if (a_over_quality != b_over_quality) { + return b_over_quality; // Inverted: if b is over-quality, a should come after b in heap + } + + // Priority 2: Among same category, prefer longer inactivity + if (p_a->inactivity_msec != p_b->inactivity_msec) { + return p_a->inactivity_msec < p_b->inactivity_msec; // Inverted + } + + // Priority 3: Among equally inactive, prefer larger memory footprint + if (p_a->bytes != p_b->bytes) { + return p_a->bytes < p_b->bytes; // Inverted + } + + // Priority 4: Break ties by targeting higher quality first (lower mip = reduce first) + if (p_a->target_mip != p_b->target_mip) { + return p_a->target_mip > p_b->target_mip; // Inverted (lower mip should be reduced first) + } + + // Priority 5: Final tie-breaker using stable pointer address for deterministic ordering. + // This prevents oscillation when all other priorities are equal. + return p_a->state < p_b->state; + } + }; + + LocalVector reduction_heap; // Heap of candidate pointers for efficient budget fitting + + static void _feedback_buffer_thread_func(void *p_udata); + void _feedback_buffer_thread_main(); + void _feedback_buffer_process(uint64_t p_ticks_msec); + + // I/O thread for texture reloading - uses its own command queue + SimpleCommandQueue io_command_queue; + Thread texture_reload_thread; + + static void _texture_reload_thread_func(void *p_udata); + void _texture_reload_thread_main(); + void _do_texture_reload(RID p_state_rid); + void _texture_remove_finalize(RID p_state_rid); + + uint64_t next_io_op_ticks = 0; + std::atomic texture_streaming_total_memory = 0; + + void _feedback_handle_data(const PackedByteArray &p_array, RID p_buffer); + + RID current_feedback_buffer; + void _feedback_frame_done_callback(); + void _feedback_frame_done_callback_render_thread(); + RID _feedback_buffer_get_next(); + + std::atomic initialized{ false }; + std::atomic flushing_count{ 0 }; + + // Completion helper for synchronous command queue calls. + // Used to wait for a result from a command executed on the feedback thread. + template + struct Completion { + BinaryMutex mutex; + ConditionVariable condvar; + bool done = false; + T result; + + void wait() { + MutexLock lock(mutex); + while (!done) { + condvar.wait(lock); + } + } + + void complete(const T &p_result) { + MutexLock lock(mutex); + result = p_result; + done = true; + condvar.notify_all(); + } + }; + + // Separate struct for void completion (commands that don't return a value). + struct CompletionVoid { + BinaryMutex mutex; + ConditionVariable condvar; + bool done = false; + + void wait() { + MutexLock lock(mutex); + while (!done) { + condvar.wait(lock); + } + } + + void complete() { + MutexLock lock(mutex); + done = true; + condvar.notify_all(); + } + }; + + mutable SimpleCommandQueue command_queue; + + // Push a command and wait for a return value. Runs inline if the processing thread is not active. + template + _FORCE_INLINE_ R push_and_wait(const char *p_method_name, T *p_instance, M p_method, Args &&...p_args) { + Completion completion; + if (!feedback_buffer_thread.is_started()) { + (p_instance->*p_method)(&completion, std::forward(p_args)...); + } else { + command_queue.push_internal(p_method_name, p_instance, p_method, &completion, std::forward(p_args)...); + completion.wait(); + } + return completion.result; + } + + // Push a command and wait for completion. Runs inline if the processing thread is not active. + template + _FORCE_INLINE_ void push_and_sync(const char *p_method_name, T *p_instance, M p_method, Args &&...p_args) { + CompletionVoid completion; + if (!feedback_buffer_thread.is_started()) { + (p_instance->*p_method)(&completion, std::forward(p_args)...); + } else { + command_queue.push_internal(p_method_name, p_instance, p_method, &completion, std::forward(p_args)...); + completion.wait(); + } + } + + // Internal implementations usually called on the feedback thread. + void _texture_configure_streaming_impl(Completion *p_completion, RID p_texture, Image::Format p_format, int p_width, int p_height, int p_min_lod, int p_max_lod, const Callable &p_reload_callable); + void _texture_update_impl(CompletionVoid *p_completion, RID p_rid, int p_width, int p_height, int p_min_lod, int p_max_lod); + void _texture_remove_impl(CompletionVoid *p_completion, RID p_rid); + + void _process_material_feedback_buffer(MaterialFeedbackBuffer *p_mb, uint64_t p_ticks_msec); + + void _render_thread_specific_initialization(); + + void _texture_replace_optimized(StreamingState *p_state, uint16_t p_resolution); + + void _on_settings_changed(); + + void _start_streaming(); + void _stop_streaming(); + + void _flush_texture_streaming_impl(); + void _flush_fence(); + void _emit_flush_completed(); + +public: + static TextureStreaming *get_singleton(); + + // Feedback Buffer API + uint32_t feedback_buffer_material_index(RID p_material); + RID feedback_buffer_get_uniform_rid(); + + // Texture API - safe from any thread + RID texture_configure_streaming(RID p_texture, Image::Format p_format, int p_width, int p_height, int p_min_lod = 0, int p_max_lod = 0, Callable p_reload_callable = Callable()); + void texture_update(RID p_rid, int p_width, int p_height, int p_min_lod, int p_max_lod); + void texture_remove(RID p_rid); + + // Material API + RID material_set_textures(RID p_feedback_rid, const Vector &p_textures); + + // Status API + uint64_t get_memory_budget_bytes_used(); + + // Properties + void set_min_lod_override(int32_t p_mip) { + setting_min_lod_override = (p_mip >= int32_t(MIN_LOD_LEVEL) && p_mip <= int32_t(MAX_LOD_LEVEL)) ? uint8_t(p_mip) : UINT8_MAX; + } + + int32_t get_min_lod_override() const { + return setting_min_lod_override != UINT8_MAX ? int32_t(setting_min_lod_override) : -1; + } + + void set_max_lod_override(int32_t p_mip) { + setting_max_lod_override = (p_mip >= int32_t(MIN_LOD_LEVEL) && p_mip <= int32_t(MAX_LOD_LEVEL)) ? uint8_t(p_mip) : UINT8_MAX; + } + + int32_t get_max_lod_override() const { + return setting_max_lod_override != UINT8_MAX ? int32_t(setting_max_lod_override) : -1; + } + + void set_memory_budget_mb_override(uint32_t p_mb) { + setting_budget_mb_override = p_mb; + } + + uint32_t get_memory_budget_mb_override() const { + return setting_budget_mb_override; + } + + void flush_texture_streaming(); + + TextureStreaming(); + virtual ~TextureStreaming(); +}; + +#endif // MODULE_TEXTURE_STREAMING_ENABLED diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 82f792a7774b..64dde104da9e 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -656,7 +656,8 @@ bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, c ".webp", // Same reasoning as .png ".cfb", // Don't let small config files slow-down startup ".scn", // Binary scenes are usually already compressed - ".ctex", // Streamable textures are usually already compressed + ".ctex", // Compressed textures are usually already compressed + ".stex", // Streamable textures are already compressed ".pck", // Pack. // Trailer for easier processing nullptr diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp index fcf09c35c883..dacdb44cb18c 100644 --- a/scene/3d/decal.cpp +++ b/scene/3d/decal.cpp @@ -240,7 +240,7 @@ void Decal::_bind_methods() { ADD_GROUP("Textures", "texture_"); // Only allow texture types that display correctly. - const String texture_hint = "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture"; + const String texture_hint = "Texture2D,-AnimatedTexture,-AtlasTexture,-CameraTexture,-CanvasTexture,-MeshTexture,-Texture2DRD,-ViewportTexture,-StreamedTexture2D"; ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_albedo", PROPERTY_HINT_RESOURCE_TYPE, texture_hint), "set_texture", "get_texture", TEXTURE_ALBEDO); ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_normal", PROPERTY_HINT_RESOURCE_TYPE, texture_hint), "set_texture", "get_texture", TEXTURE_NORMAL); ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "texture_orm", PROPERTY_HINT_RESOURCE_TYPE, texture_hint), "set_texture", "get_texture", TEXTURE_ORM); diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 56e337f8ed12..e7aee4f4a574 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -157,6 +157,7 @@ #include "scene/resources/shader_resource_format.h" #include "scene/resources/skeleton_profile.h" #include "scene/resources/sky.h" +#include "scene/resources/streamed_texture.h" #include "scene/resources/style_box.h" #include "scene/resources/style_box_flat.h" #include "scene/resources/style_box_line.h" @@ -378,7 +379,8 @@ static Ref resource_saver_text; static Ref resource_loader_text; -static Ref resource_loader_stream_texture; +static Ref resource_loader_compressed_texture; +static Ref resource_loader_streamed_texture; static Ref resource_loader_texture_layered; static Ref resource_loader_texture_3d; @@ -398,8 +400,13 @@ void register_scene_types() { Node::init_node_hrcr(); if constexpr (GD_IS_CLASS_ENABLED(CompressedTexture2D)) { - resource_loader_stream_texture.instantiate(); - ResourceLoader::add_resource_format_loader(resource_loader_stream_texture); + resource_loader_compressed_texture.instantiate(); + ResourceLoader::add_resource_format_loader(resource_loader_compressed_texture); + } + + if constexpr (GD_IS_CLASS_ENABLED(StreamedTexture2D)) { + resource_loader_streamed_texture.instantiate(); + ResourceLoader::add_resource_format_loader(resource_loader_streamed_texture); } if constexpr (GD_IS_CLASS_ENABLED(TextureLayered)) { @@ -935,6 +942,7 @@ void register_scene_types() { GDREGISTER_CLASS(World2D); GDREGISTER_CLASS(Sky); GDREGISTER_CLASS(CompressedTexture2D); + GDREGISTER_CLASS(StreamedTexture2D); GDREGISTER_CLASS(PortableCompressedTexture2D); GDREGISTER_CLASS(ImageTexture); GDREGISTER_CLASS(AtlasTexture); @@ -1329,8 +1337,13 @@ void unregister_scene_types() { } if constexpr (GD_IS_CLASS_ENABLED(CompressedTexture2D)) { - ResourceLoader::remove_resource_format_loader(resource_loader_stream_texture); - resource_loader_stream_texture.unref(); + ResourceLoader::remove_resource_format_loader(resource_loader_compressed_texture); + resource_loader_compressed_texture.unref(); + } + + if constexpr (GD_IS_CLASS_ENABLED(StreamedTexture2D)) { + ResourceLoader::remove_resource_format_loader(resource_loader_streamed_texture); + resource_loader_streamed_texture.unref(); } ResourceSaver::remove_resource_format_saver(resource_saver_text); diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index 2de6e7e84f74..5d922e43bc12 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -2035,6 +2035,24 @@ void fragment() {)"; )"; } + bool streaming_enabled = false; +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + streaming_enabled = GLOBAL_GET_CACHED(bool, "rendering/textures/streaming/enabled"); +#endif + if (streaming_enabled && flags[FLAG_UV1_USE_TRIPLANAR]) { + code += R"( + // Write triplanar UV to UV for texture streaming feedback. + // Pick the UV projection of the dominant triplanar axis. + if (uv1_power_normal.x >= uv1_power_normal.y && uv1_power_normal.x >= uv1_power_normal.z) { + STREAMING_UV = uv1_triplanar_pos.zy * vec2(-1.0, 1.0); + } else if (uv1_power_normal.y >= uv1_power_normal.z) { + STREAMING_UV = uv1_triplanar_pos.xz; + } else { + STREAMING_UV = uv1_triplanar_pos.xy; + } +)"; + } + code += "}\n"; // We must create the shader outside the shader_map_mutex to avoid potential deadlocks with diff --git a/scene/resources/streamed_texture.cpp b/scene/resources/streamed_texture.cpp new file mode 100644 index 000000000000..f77d5b46243b --- /dev/null +++ b/scene/resources/streamed_texture.cpp @@ -0,0 +1,639 @@ +/**************************************************************************/ +/* streamed_texture.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "streamed_texture.h" + +#include "core/config/project_settings.h" +#include "core/io/file_access.h" +#include "core/io/image.h" +#include "core/object/callable_mp.h" +#include "core/object/class_db.h" +#include "core/os/os.h" +#include "core/profiling/profiling.h" +#include "core/string/print_string.h" +#include "scene/resources/bit_map.h" +#include "scene/resources/compressed_texture.h" +#include "servers/rendering/rendering_server.h" + +#include "modules/modules_enabled.gen.h" + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED +#include "modules/texture_streaming/texture_streaming.h" +#endif + +StreamedTexture2D::TextureFormatRoughnessRequestCallback StreamedTexture2D::request_roughness_callback = nullptr; +StreamedTexture2D::TextureFormatRequestCallback StreamedTexture2D::request_normal_callback = nullptr; + +void StreamedTexture2D::_requested_roughness(void *p_ud, const String &p_normal_path, RSE::TextureDetectRoughnessChannel p_roughness_channel) { + StreamedTexture2D *ct = (StreamedTexture2D *)p_ud; + Ref ctex(ct); + ERR_FAIL_NULL(request_roughness_callback); + request_roughness_callback(ctex, p_normal_path, p_roughness_channel); +} + +void StreamedTexture2D::_requested_normal(void *p_ud) { + StreamedTexture2D *ct = (StreamedTexture2D *)p_ud; + Ref ctex(ct); + ERR_FAIL_NULL(request_normal_callback); + request_normal_callback(ctex); +} + +void StreamedTexture2D::texture_reload(uint8_t p_mip_level) { + GodotProfileZone("StreamedTexture2D::texture_reload"); + + ERR_FAIL_COND(texture.is_null()); + ERR_FAIL_COND(path_to_file.is_empty()); + + const bool verbose_logging = OS::get_singleton()->is_stdout_verbose(); + uint64_t start_time = 0; + if (verbose_logging) { + start_time = OS::get_singleton()->get_ticks_msec(); + } + + // Clamp cached current mip to what is actually available in file. + uint32_t current_mip = MIN((uint32_t)_current_mip.load(), num_mipmaps); + if (current_mip != _current_mip.load()) { + _current_mip.store((uint8_t)current_mip); + } + + uint32_t new_mip = MIN((uint32_t)p_mip_level, num_mipmaps); + + // If the requested mip level is the same as the current, do nothing. + if (current_mip == new_mip) { + return; + } + + _current_mip.store((uint8_t)new_mip); // Atomic store + + // Recreate the texture at the new mip level. + StreamedTexture2DLoadData load_data; + load_data.image.instantiate(); + Error err = _load_data(path_to_file, _current_mip.load(), load_data); + if (err != OK) { + ERR_PRINT(vformat("Failed to load texture data for %s. Falling back to full reload.", path_to_file)); + _load_internal(path_to_file, false); + return; + } + + RID new_texture = RS::get_singleton()->texture_2d_create(load_data.image); + if (new_texture.is_null()) { + ERR_PRINT(vformat("Failed to create new texture for %s. Falling back to full reload.", path_to_file)); + _load_internal(path_to_file, false); + return; + } + + RenderingServer::get_singleton()->texture_set_path(new_texture, path_to_file); + RS::get_singleton()->texture_replace_compatible(texture, new_texture); + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + // Re-attach streaming state after texture replacement if streaming is active. + if (use_streaming && streaming_state.is_valid()) { + RS::get_singleton()->texture_2d_attach_streaming_state(texture, streaming_state); + } +#endif + + if (verbose_logging) { + const uint64_t end_time = OS::get_singleton()->get_ticks_msec(); + print_verbose(vformat("Streamed texture %s reloaded (%dx%d) in %d ms (start=%d, end=%d)", path_to_file, w, h, end_time - start_time, start_time, end_time)); + } +} + +Error StreamedTexture2D::_parse_header(const String &p_path, StreamedTextureFileHeader &r_header, Ref &r_file) { + Ref f = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_OPEN, vformat("Unable to open file: %s.", p_path)); + + // Check for Godot Streamed Texture Format header (ie, GSTF). + uint8_t header[4]; + f->get_buffer(header, 4); + if (header[0] != 'G' || header[1] != 'S' || header[2] != 'T' || header[3] != 'F') { + ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Streamed texture file is corrupt (Bad header)."); + } + + // Version. + uint32_t version = f->get_32(); + constexpr uint32_t FORMAT_VERSION = 1; + if (version > FORMAT_VERSION) { + ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Streamed texture file is too new."); + } + + // Read width, height. + r_header.width = f->get_32(); + r_header.height = f->get_32(); + + // Read data size. + r_header.data_size = f->get_32(); + + // Read image format. + uint32_t format_value = f->get_32(); + ERR_FAIL_COND_V_MSG(format_value >= Image::FORMAT_MAX, ERR_FILE_CORRUPT, "Streamed texture has invalid image format."); + r_header.format = (Image::Format)format_value; + + // Read mipmap count. + r_header.num_mipmaps = f->get_32(); + ERR_FAIL_COND_V_MSG(r_header.num_mipmaps > 16, ERR_FILE_CORRUPT, "Streamed texture has too many mipmaps."); + + // Read the offset and size for each mip level. + r_header.mipmap_offset_size.resize(r_header.num_mipmaps + 1); + for (uint32_t i = 0; i <= r_header.num_mipmaps; i++) { + r_header.mipmap_offset_size[i].first = f->get_32(); + r_header.mipmap_offset_size[i].second = f->get_32(); + } + + // Read flags. + r_header.flags = f->get_32(); + + // Read streaming settings. + r_header.streaming_min = f->get_32(); + r_header.streaming_max = f->get_32(); + + // Reserved for future use. + f->get_32(); + + r_file = f; + return OK; +} + +Error StreamedTexture2D::_load_mips_from_file(Ref p_file, const StreamedTextureFileHeader &p_header, uint32_t p_start_mip, uint32_t p_mip_count, Vector &r_mip_data) { + ERR_FAIL_COND_V_MSG( + p_start_mip + p_mip_count > p_header.num_mipmaps + 1, + ERR_INVALID_PARAMETER, + vformat("Requested mip range [%d, %d) exceeds available mip range [0, %d).", int(p_start_mip), int(p_start_mip + p_mip_count), int(p_header.num_mipmaps + 1))); + + uint64_t data_start = p_file->get_position(); + + // Calculate total size needed for all requested mips. + uint32_t total_size = 0; + for (uint32_t i = 0; i < p_mip_count; i++) { + total_size += p_header.mipmap_offset_size[p_start_mip + i].second; + } + + // Load all mip data into a single buffer. + r_mip_data.resize(total_size); + uint32_t write_offset = 0; + for (uint32_t i = 0; i < p_mip_count; i++) { + uint32_t mip_index = p_start_mip + i; + uint32_t mip_offset = p_header.mipmap_offset_size[mip_index].first; + uint32_t mip_size = p_header.mipmap_offset_size[mip_index].second; + + p_file->seek(data_start + mip_offset); + uint64_t bytes_read = p_file->get_buffer(r_mip_data.ptrw() + write_offset, mip_size); + ERR_FAIL_COND_V_MSG(bytes_read != mip_size, ERR_FILE_CORRUPT, "Streamed texture file is truncated."); + write_offset += mip_size; + } + + return OK; +} + +Error StreamedTexture2D::_load_data(const String &p_path, const uint8_t p_mip_level, StreamedTexture2DLoadData &p_load_data) { + ERR_FAIL_COND_V(p_load_data.image.is_null(), ERR_INVALID_PARAMETER); + + StreamedTextureFileHeader header; + Ref f; + Error err = _parse_header(p_path, header, f); + ERR_FAIL_COND_V(err != OK, err); + + // Validate dimensions. + ERR_FAIL_COND_V_MSG(header.width == 0 || header.height == 0, ERR_FILE_CORRUPT, "Streamed texture has invalid dimensions."); + ERR_FAIL_COND_V_MSG(header.width > 16384 || header.height > 16384, ERR_FILE_CORRUPT, "Streamed texture dimensions are too large."); + + // Populate load data from header. + p_load_data.width = header.width; + p_load_data.height = header.height; + p_load_data.format = header.format; + p_load_data.mipmap_count = header.num_mipmaps; + p_load_data.streaming_min = header.streaming_min; + p_load_data.streaming_max = header.streaming_max; + +#ifdef TOOLS_ENABLED + p_load_data.request_normal = request_normal_callback && (header.flags & FORMAT_BIT_DETECT_NORMAL); + p_load_data.request_roughness = request_roughness_callback && (header.flags & FORMAT_BIT_DETECT_ROUGHNESS); +#else + p_load_data.request_normal = false; + p_load_data.request_roughness = false; +#endif + + // Validate that the file contains enough data. + uint64_t header_end = f->get_position(); + uint64_t file_length = f->get_length(); + ERR_FAIL_COND_V_MSG(file_length < header_end + header.data_size, ERR_FILE_CORRUPT, "Streamed texture file is truncated (file too small for declared data size)."); + + // Clamp the mip level to valid range. + uint32_t load_mip = MIN((uint32_t)p_mip_level, header.num_mipmaps); + uint32_t mip_count = header.num_mipmaps - load_mip; + + // Load mip data from file. + Vector data; + err = _load_mips_from_file(f, header, load_mip, mip_count + 1, data); + ERR_FAIL_COND_V(err != OK, err); + + // Calculate the dimensions at this mip level. + uint32_t mip_width = MAX(1u, header.width >> load_mip); + uint32_t mip_height = MAX(1u, header.height >> load_mip); + + p_load_data.image->set_data(mip_width, mip_height, mip_count > 0, header.format, data); + + return OK; +} + +Error StreamedTexture2D::_save_data(const String &p_path, const Ref &p_image, uint32_t p_flags, uint32_t p_streaming_min, uint32_t p_streaming_max) { + ERR_FAIL_COND_V(p_image.is_null(), ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(p_image->is_empty(), ERR_INVALID_PARAMETER); + + Ref f = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_CREATE, vformat("Unable to create file: %s.", p_path)); + + // Write Godot Streamed Texture Format header (ie, GSTF). + f->store_8('G'); + f->store_8('S'); + f->store_8('T'); + f->store_8('F'); + + // Version. + constexpr uint32_t FORMAT_VERSION = 1; + f->store_32(FORMAT_VERSION); + + // Write width, height. + f->store_32(p_image->get_width()); + f->store_32(p_image->get_height()); + + // Get image data. + Vector data = p_image->get_data(); + + // Write data size. + f->store_32(data.size()); + + // Write image format. + f->store_32((uint32_t)p_image->get_format()); + + // Write mipmap count. + uint32_t num_mipmaps = p_image->get_mipmap_count(); + f->store_32(num_mipmaps); + + // Write the offset and size for each mip level. + for (uint32_t i = 0; i <= num_mipmaps; i++) { + int64_t mip_offset, mip_size; + p_image->get_mipmap_offset_and_size(i, mip_offset, mip_size); + f->store_32((uint32_t)mip_offset); + f->store_32((uint32_t)mip_size); + } + + // Write flags (request_normal, request_roughness). + f->store_32(p_flags); + + // Write streaming min size. + f->store_32(p_streaming_min); + + // Write streaming max size. + f->store_32(p_streaming_max); + + // Reserved for future use. + f->store_32(0); + + // Write image data. + f->store_buffer(data.ptr(), data.size()); + + return OK; +} + +uint8_t StreamedTexture2D::_get_initial_streaming_mip(uint32_t p_streaming_min, uint32_t p_streaming_max) const { +#ifndef MODULE_TEXTURE_STREAMING_ENABLED + return 0; +#else + // MAX_LOD_LEVEL is duplicated so this file still builds with the texture + // streaming module disabled; the two must agree when it is enabled. + static_assert(MAX_LOD_LEVEL == int(TextureStreaming::MAX_LOD_LEVEL), + "StreamedTexture2D::MAX_LOD_LEVEL must match TextureStreaming::MAX_LOD_LEVEL."); + + uint8_t min_lod = p_streaming_min > 0 ? uint8_t(CLAMP(p_streaming_min - 1, 0u, uint32_t(MAX_LOD_LEVEL))) : uint8_t(CLAMP(int(GLOBAL_GET("rendering/textures/streaming/min_lod")), 0, MAX_LOD_LEVEL)); + uint8_t max_lod = p_streaming_max > 0 ? uint8_t(CLAMP(p_streaming_max - 1, 0u, uint32_t(MAX_LOD_LEVEL))) : uint8_t(CLAMP(int(GLOBAL_GET("rendering/textures/streaming/max_lod")), 0, MAX_LOD_LEVEL)); + return MAX(min_lod, max_lod); +#endif +} + +Error StreamedTexture2D::_load_internal(const String &p_path, bool p_load_settings) { + ERR_FAIL_COND_V(p_path.is_empty(), ERR_FILE_BAD_PATH); + alpha_cache.unref(); + + if (p_load_settings && use_streaming) { + StreamedTextureFileHeader header; + Ref header_file; + Error header_err = _parse_header(p_path, header, header_file); + ERR_FAIL_COND_V(header_err != OK, header_err); + _current_mip.store(_get_initial_streaming_mip(header.streaming_min, header.streaming_max)); + } + + StreamedTexture2DLoadData load_data; + load_data.image.instantiate(); + Error err = _load_data(p_path, _current_mip.load(), load_data); + ERR_FAIL_COND_V(err != OK, err); + + RID new_texture = RS::get_singleton()->texture_2d_create(load_data.image); + RenderingServer::get_singleton()->texture_set_path(new_texture, p_path); + path_to_file = p_path; + + if (texture.is_valid()) { + RS::get_singleton()->texture_replace(texture, new_texture); + } else { + texture = new_texture; + } + + w = load_data.width; + h = load_data.height; + format = load_data.format; + // Use the mip count declared in file (not a computed full chain), as some files may not include all mips. + num_mipmaps = load_data.mipmap_count; + _current_mip.store(uint8_t(MIN((uint32_t)_current_mip.load(), num_mipmaps))); +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + if (use_streaming) { + if (p_load_settings) { + // streaming_min in file = best quality limit (min mip), streaming_max = worst quality limit (max mip) + min_lod_override = load_data.streaming_min; + max_lod_override = load_data.streaming_max; + } + + if (streaming_state.is_null()) { + streaming_state = TextureStreaming::get_singleton()->texture_configure_streaming( + texture, + format, + w, + h, + min_lod_override, + max_lod_override, + callable_mp(this, &StreamedTexture2D::texture_reload)); + } else { + TextureStreaming::get_singleton()->texture_update(streaming_state, w, h, min_lod_override, max_lod_override); + } + } +#endif + + if (p_load_settings) { +#ifdef TOOLS_ENABLED + if (load_data.request_roughness) { + RS::get_singleton()->texture_set_detect_roughness_callback(texture, _requested_roughness, this); + } else { + RS::get_singleton()->texture_set_detect_roughness_callback(texture, nullptr, nullptr); + } + + if (load_data.request_normal) { + RS::get_singleton()->texture_set_detect_normal_callback(texture, _requested_normal, this); + } else { + RS::get_singleton()->texture_set_detect_normal_callback(texture, nullptr, nullptr); + } +#endif + + notify_property_list_changed(); + emit_changed(); + } + + return OK; +} + +Error StreamedTexture2D::load(const String &p_path) { + return _load_internal(p_path, true); +} + +// Called when LOD settings change or after loading. +// Part of the reason this is done is to work around a limitation in the current texture +// system in godot. There are no notifications when a texture is changed. So the only way +// for the editor to update the displayed texture after changing LOD settings is to "change" +// the texture so just do that. +void StreamedTexture2D::update_texture() { + _load_internal(path_to_file, false); +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + if (streaming_state.is_valid()) { + TextureStreaming::get_singleton()->texture_update(streaming_state, w, h, min_lod_override, max_lod_override); + } +#endif +} + +void StreamedTexture2D::reload_from_file() { + String path = get_path(); + if (!path.is_resource_file()) { + return; + } + + path = ResourceLoader::path_remap(path); //remap for translation + path = ResourceLoader::import_remap(path); //remap for import + if (!path.is_resource_file()) { + return; + } + + load(path); +} + +void StreamedTexture2D::_bind_methods() { + ClassDB::bind_method(D_METHOD("load", "path"), &StreamedTexture2D::load); + ClassDB::bind_method(D_METHOD("get_load_path"), &StreamedTexture2D::get_load_path); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "load_path", PROPERTY_HINT_FILE, "*.stex"), "load", "get_load_path"); + + ClassDB::bind_method(D_METHOD("set_max_lod_override", "max_lod"), &StreamedTexture2D::set_max_lod_override); + ClassDB::bind_method(D_METHOD("get_max_lod_override"), &StreamedTexture2D::get_max_lod_override); + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_lod_override", PROPERTY_HINT_ENUM, "Setting Default,0,1,2,3,4,5,6,7,8,9,10,11,12,13"), "set_max_lod_override", "get_max_lod_override"); + + ClassDB::bind_method(D_METHOD("set_min_lod_override", "min_lod"), &StreamedTexture2D::set_min_lod_override); + ClassDB::bind_method(D_METHOD("get_min_lod_override"), &StreamedTexture2D::get_min_lod_override); + ADD_PROPERTY(PropertyInfo(Variant::INT, "min_lod_override", PROPERTY_HINT_ENUM, "Setting Default,0,1,2,3,4,5,6,7,8,9,10,11,12,13"), "set_min_lod_override", "get_min_lod_override"); +} + +void StreamedTexture2D::set_max_lod_override(int p_max_lod) { + max_lod_override = p_max_lod; + update_texture(); +} + +int StreamedTexture2D::get_max_lod_override() const { + return max_lod_override; +} + +void StreamedTexture2D::set_min_lod_override(int p_min_lod) { + min_lod_override = p_min_lod; + update_texture(); +} + +int StreamedTexture2D::get_min_lod_override() const { + return min_lod_override; +} + +Image::Format StreamedTexture2D::get_format() const { + return format; +} + +String StreamedTexture2D::get_load_path() const { + return path_to_file; +} + +int StreamedTexture2D::get_width() const { + return w; +} + +int StreamedTexture2D::get_height() const { + return h; +} + +RID StreamedTexture2D::get_rid() const { + if (!texture.is_valid()) { + texture = RS::get_singleton()->texture_2d_placeholder_create(); + } + return texture; +} + +void StreamedTexture2D::set_path(const String &p_path, bool p_take_over) { + if (texture.is_valid()) { + RenderingServer::get_singleton()->texture_set_path(texture, p_path); + } + + Resource::set_path(p_path, p_take_over); +} + +void StreamedTexture2D::draw(RID p_canvas_item, const Point2 &p_pos, const Color &p_modulate, bool p_transpose) const { + if ((w | h) == 0) { + return; + } + RenderingServer::get_singleton()->canvas_item_add_texture_rect(p_canvas_item, Rect2(p_pos, Size2(w, h)), texture, false, p_modulate, p_transpose); +} + +void StreamedTexture2D::draw_rect(RID p_canvas_item, const Rect2 &p_rect, bool p_tile, const Color &p_modulate, bool p_transpose) const { + if ((w | h) == 0) { + return; + } + RenderingServer::get_singleton()->canvas_item_add_texture_rect(p_canvas_item, p_rect, texture, p_tile, p_modulate, p_transpose); +} + +void StreamedTexture2D::draw_rect_region(RID p_canvas_item, const Rect2 &p_rect, const Rect2 &p_src_rect, const Color &p_modulate, bool p_transpose, bool p_clip_uv) const { + if ((w | h) == 0) { + return; + } + RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas_item, p_rect, texture, p_src_rect, p_modulate, p_transpose, p_clip_uv); +} + +bool StreamedTexture2D::has_alpha() const { + return false; +} + +Ref StreamedTexture2D::get_image() const { + return _load_image(path_to_file, 0); +} + +bool StreamedTexture2D::is_pixel_opaque(int p_x, int p_y) const { + if (alpha_cache.is_null()) { + Ref img = get_image(); + if (img.is_valid()) { + if (img->is_compressed()) { //must decompress, if compressed + Ref decom = img->duplicate(); + decom->decompress(); + img = decom; + } + + alpha_cache.instantiate(); + alpha_cache->create_from_image_alpha(img); + } + } + + if (alpha_cache.is_valid()) { + int aw = int(alpha_cache->get_size().width); + int ah = int(alpha_cache->get_size().height); + if (aw == 0 || ah == 0) { + return true; + } + + int x = p_x * aw / w; + int y = p_y * ah / h; + + x = CLAMP(x, 0, aw - 1); + y = CLAMP(y, 0, ah - 1); + + return alpha_cache->get_bit(x, y); + } + + return true; +} + +StreamedTexture2D::StreamedTexture2D() { + max_lod_override = 0; + min_lod_override = 0; + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + const bool streaming_enabled = GLOBAL_GET("rendering/textures/streaming/enabled"); + const String rendering_method = OS::get_singleton()->get_current_rendering_method(); + use_streaming = streaming_enabled && rendering_method != "gl_compatibility"; + if (!use_streaming) { + _current_mip.store(0); // force full resolution (mip 0) + } else { + // Initial mip level from settings (higher mip = lower quality initially) + _current_mip.store(uint8_t(GLOBAL_GET("rendering/textures/streaming/max_lod"))); + } +#else + use_streaming = false; + _current_mip.store(0); // force full resolution (mip 0) +#endif +} + +StreamedTexture2D::~StreamedTexture2D() { +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + if (streaming_state.is_valid()) { + TextureStreaming::get_singleton()->texture_remove(streaming_state); + streaming_state = RID(); + } +#endif + + if (texture.is_valid()) { + ERR_FAIL_NULL(RenderingServer::get_singleton()); + RS::get_singleton()->free_rid(texture); + } +} + +Ref ResourceFormatLoaderStreamedTexture2D::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { + Ref st; + st.instantiate(); + + Error err = st->load(p_path); + if (r_error) { + *r_error = err; + } + if (err != OK) { + return Ref(); + } + + return st; +} + +void ResourceFormatLoaderStreamedTexture2D::get_recognized_extensions(List *p_extensions) const { + p_extensions->push_back("stex"); +} + +bool ResourceFormatLoaderStreamedTexture2D::handles_type(const String &p_type) const { + return p_type == "StreamedTexture2D"; +} + +String ResourceFormatLoaderStreamedTexture2D::get_resource_type(const String &p_path) const { + if (p_path.get_extension().to_lower() == "stex") { + return "StreamedTexture2D"; + } + return ""; +} diff --git a/scene/resources/streamed_texture.h b/scene/resources/streamed_texture.h new file mode 100644 index 000000000000..92a4ab2ec2ea --- /dev/null +++ b/scene/resources/streamed_texture.h @@ -0,0 +1,175 @@ +/**************************************************************************/ +/* streamed_texture.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/io/resource_loader.h" +#include "scene/resources/texture.h" +#include "servers/rendering/rendering_server_types.h" + +#include + +class BitMap; +class ResourceImporterStreamedTexture; + +class StreamedTexture2D : public Texture2D { + GDCLASS(StreamedTexture2D, Texture2D); + + // Maximum LOD level supported (13 = ~8192px down to 1px) + static constexpr int MAX_LOD_LEVEL = 13; + + struct StreamedTexture2DLoadData { + uint32_t width = 0; + uint32_t height = 0; + uint32_t mipmap_count = 0; + Image::Format format = Image::FORMAT_DXT1; + Ref image; + bool request_normal = false; + bool request_roughness = false; + uint32_t streaming_min = 0; + uint32_t streaming_max = 0; + }; + + struct StreamedTextureFileHeader { + uint32_t width = 0; + uint32_t height = 0; + uint32_t data_size = 0; + Image::Format format = Image::FORMAT_DXT1; + uint32_t num_mipmaps = 0; + LocalVector> mipmap_offset_size; + uint32_t flags = 0; + uint32_t streaming_min = 0; + uint32_t streaming_max = 0; + }; + + enum FormatBits { + FORMAT_BIT_DETECT_NORMAL = 1 << 0, + FORMAT_BIT_DETECT_ROUGHNESS = 1 << 1, + }; + + static Error _parse_header(const String &p_path, StreamedTextureFileHeader &r_header, Ref &r_file); + static Error _load_mips_from_file(Ref p_file, const StreamedTextureFileHeader &p_header, uint32_t p_start_mip, uint32_t p_mip_count, Vector &r_mip_data); + static Error _load_data(const String &p_path, const uint8_t p_mip_level, struct StreamedTexture2DLoadData &p_load_data); + static Error _save_data(const String &p_path, const Ref &p_image, uint32_t p_flags, uint32_t p_streaming_min, uint32_t p_streaming_max); + uint8_t _get_initial_streaming_mip(uint32_t p_streaming_min, uint32_t p_streaming_max) const; + + Ref _load_image(const String &p_path, uint8_t p_mip_level) const { + StreamedTexture2DLoadData load_data; + load_data.image = Ref(); + load_data.image.instantiate(); + + Error err = _load_data(p_path, p_mip_level, load_data); + if (err != OK) { + return Ref(); + } + + return load_data.image; + } + + void _save_image(const String &p_path, const Ref &p_image) const { + _save_data(p_path, p_image, 0, 0, 0); + } + + void update_texture(); + void texture_reload(uint8_t p_mip_level); + std::atomic _current_mip = 0; // Atomic for thread-safe access from I/O thread + bool use_streaming = false; + + RID streaming_state; + + // LOD override settings (0 = use default from project settings) + int min_lod_override = 0; + int max_lod_override = 0; + + Error _load_internal(const String &p_path, bool p_load_settings = true); + + String path_to_file; + mutable RID texture; + Image::Format format = Image::FORMAT_L8; + int w = 0; + int h = 0; + uint32_t num_mipmaps = 0; + mutable Ref alpha_cache; + + typedef void (*TextureFormatRequestCallback)(const Ref &); + typedef void (*TextureFormatRoughnessRequestCallback)(const Ref &, const String &p_normal_path, RSE::TextureDetectRoughnessChannel p_roughness_channel); + + static void _requested_roughness(void *p_ud, const String &p_normal_path, RSE::TextureDetectRoughnessChannel p_roughness_channel); + static void _requested_normal(void *p_ud); + +protected: + static void _bind_methods(); + virtual void reload_from_file() override; + +public: + static TextureFormatRoughnessRequestCallback request_roughness_callback; + static TextureFormatRequestCallback request_normal_callback; + + void set_max_lod_override(int p_max_lod); + int get_max_lod_override() const; + + void set_min_lod_override(int p_min_lod); + int get_min_lod_override() const; + + Error load(const String &p_path); + + virtual Image::Format get_format() const override; + String get_load_path() const; + + virtual int get_width() const override; + virtual int get_height() const override; + virtual RID get_rid() const override; + + virtual void set_path(const String &p_path, bool p_take_over) override; + + virtual void draw(RID p_canvas_item, const Point2 &p_pos, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false) const override; + virtual void draw_rect(RID p_canvas_item, const Rect2 &p_rect, bool p_tile = false, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false) const override; + virtual void draw_rect_region(RID p_canvas_item, const Rect2 &p_rect, const Rect2 &p_src_rect, const Color &p_modulate = Color(1, 1, 1), bool p_transpose = false, bool p_clip_uv = true) const override; + + virtual bool has_alpha() const override; + virtual bool is_pixel_opaque(int p_x, int p_y) const override; + + virtual Ref get_image() const override; + + StreamedTexture2D(); + ~StreamedTexture2D(); + + friend class ResourceImporterStreamedTexture; // For saving to stex format +}; + +class ResourceFormatLoaderStreamedTexture2D : public ResourceFormatLoader { + GDSOFTCLASS(ResourceFormatLoaderStreamedTexture2D, ResourceFormatLoader); + +public: + virtual Ref load(const String &p_path, const String &p_original_path = "", Error *r_error = nullptr, bool p_use_sub_threads = false, float *r_progress = nullptr, CacheMode p_cache_mode = CACHE_MODE_REUSE) override; + virtual void get_recognized_extensions(List *p_extensions) const override; + virtual bool handles_type(const String &p_type) const override; + virtual String get_resource_type(const String &p_path) const override; +}; diff --git a/servers/rendering/dummy/storage/texture_storage.h b/servers/rendering/dummy/storage/texture_storage.h index 8429d1e400de..0be05e0156dc 100644 --- a/servers/rendering/dummy/storage/texture_storage.h +++ b/servers/rendering/dummy/storage/texture_storage.h @@ -117,6 +117,7 @@ class TextureStorage : public RendererTextureStorage { virtual RID texture_drawable_get_default_material() const override { return RID(); } virtual void texture_replace(RID p_texture, RID p_by_texture) override { texture_free(p_by_texture); } + virtual void texture_replace_compatible(RID p_texture, RID p_by_texture) override { texture_free(p_by_texture); } virtual void texture_set_size_override(RID p_texture, int p_width, int p_height) override {} virtual void texture_set_path(RID p_texture, const String &p_path) override {} @@ -230,6 +231,8 @@ class TextureStorage : public RendererTextureStorage { virtual void render_target_set_velocity_target_size(RID p_render_target, const Size2i &p_target_size) override {} virtual Size2i render_target_get_velocity_target_size(RID p_render_target) const override { return Size2i(0, 0); } + + virtual void texture_2d_attach_streaming_state(RID p_texture, RID p_streaming_state) override {} }; } // namespace RendererDummy diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp index 36671e57a8cd..3d624fe2bea1 100644 --- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp @@ -42,6 +42,12 @@ #include "servers/rendering/rendering_server_default.h" #include "servers/rendering/storage/ltc_lut.gen.h" +#include "modules/modules_enabled.gen.h" + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED +#include "modules/texture_streaming/texture_streaming.h" +#endif + using namespace RendererSceneRenderImplementation; #define PRELOAD_PIPELINES_ON_SURFACE_CACHE_CONSTRUCTION 1 @@ -424,6 +430,7 @@ void RenderForwardClustered::_render_list_template(RenderingDevice::DrawListID p pipeline_specialization.multimesh_format_2d = bool(surf->owner->base_flags & INSTANCE_DATA_FLAG_MULTIMESH_FORMAT_2D); pipeline_specialization.multimesh_has_color = bool(surf->owner->base_flags & INSTANCE_DATA_FLAG_MULTIMESH_HAS_COLOR); pipeline_specialization.multimesh_has_custom_data = bool(surf->owner->base_flags & INSTANCE_DATA_FLAG_MULTIMESH_HAS_CUSTOM_DATA); + pipeline_specialization.material_feedback = p_params->use_material_feedback && surf->material->material_feedback_rid.is_valid(); if constexpr (p_pass_mode == PASS_MODE_COLOR) { pipeline_specialization.use_light_soft_shadows = element_info.uses_softshadow; @@ -870,7 +877,15 @@ void RenderForwardClustered::_fill_instance_data(RenderListType p_render_list, i uv_scale = RendererRD::MeshStorage::get_singleton()->mesh_surface_get_uv_scale(surface->surface); } + uint32_t material_feedback_index = UINT32_MAX; +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + if (surface->material->material_feedback_rid.is_valid()) { + material_feedback_index = TextureStreaming::get_singleton()->feedback_buffer_material_index(surface->material->material_feedback_rid); + } +#endif + instance_data.set_compressed_aabb(surface_aabb); + instance_data.material_feedback_index = material_feedback_index; instance_data.set_uv_scale(uv_scale); scene_state.curr_gpu_ptr[p_render_list][i + p_offset] = instance_data; @@ -2113,7 +2128,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co bool debug_voxelgis = get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_VOXEL_GI_ALBEDO || get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_VOXEL_GI_LIGHTING || get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_VOXEL_GI_EMISSION; bool debug_sdfgi_probes = get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_SDFGI_PROBES; bool force_depth_pre_pass = scene_state.used_opaque_stencil; - bool depth_pre_pass = (force_depth_pre_pass || bool(GLOBAL_GET_CACHED(bool, "rendering/driver/depth_prepass/enable"))) && depth_framebuffer.is_valid(); + bool depth_pre_pass = (force_depth_pre_pass || scene_shader.depth_prepass_enabled) && depth_framebuffer.is_valid(); SceneShaderForwardClustered::ShaderSpecialization base_specialization = scene_shader.default_specialization; base_specialization.use_depth_fog = p_render_data->environment.is_valid() && environment_get_fog_mode(p_render_data->environment) == RSE::EnvironmentFogMode::ENV_FOG_MODE_DEPTH; @@ -2140,7 +2155,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co RID rp_uniform_set = _setup_render_pass_uniform_set(RENDER_LIST_OPAQUE, nullptr, is_multiview, RID(), samplers, depth_prepass_uniform_buffer_index); bool finish_depth = using_ssao || using_ssil || using_sdfgi || using_voxelgi || ce_pre_opaque_resolved_depth || ce_post_opaque_resolved_depth; - RenderListParameters render_list_params(render_list[RENDER_LIST_OPAQUE].elements.ptr(), render_list[RENDER_LIST_OPAQUE].element_info.ptr(), render_list[RENDER_LIST_OPAQUE].elements.size(), reverse_cull, depth_pass_mode, 0, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization); + RenderListParameters render_list_params(render_list[RENDER_LIST_OPAQUE].elements.ptr(), render_list[RENDER_LIST_OPAQUE].element_info.ptr(), render_list[RENDER_LIST_OPAQUE].elements.size(), reverse_cull, depth_pass_mode, 0, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization, !is_reflection_probe); _render_list_with_draw_list(&render_list_params, depth_framebuffer, RD::DrawFlags(needs_pre_resolve ? RD::DRAW_DEFAULT_ALL : RD::DRAW_CLEAR_ALL), depth_pass_clear, 0.0f, 0u, p_render_data->render_region); RD::get_singleton()->draw_command_end_label(); @@ -2222,7 +2237,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co uint32_t opaque_color_pass_flags = using_motion_pass ? (color_pass_flags & ~uint32_t(COLOR_PASS_FLAG_MOTION_VECTORS)) : color_pass_flags; RID opaque_framebuffer = using_motion_pass ? rb_data->get_color_pass_fb(opaque_color_pass_flags) : color_framebuffer; - RenderListParameters render_list_params(render_list[RENDER_LIST_OPAQUE].elements.ptr(), render_list[RENDER_LIST_OPAQUE].element_info.ptr(), render_list[RENDER_LIST_OPAQUE].elements.size(), reverse_cull, PASS_MODE_COLOR, opaque_color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization); + RenderListParameters render_list_params(render_list[RENDER_LIST_OPAQUE].elements.ptr(), render_list[RENDER_LIST_OPAQUE].element_info.ptr(), render_list[RENDER_LIST_OPAQUE].elements.size(), reverse_cull, PASS_MODE_COLOR, opaque_color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization, !is_reflection_probe); _render_list_with_draw_list(&render_list_params, opaque_framebuffer, RD::DrawFlags(load_color ? RD::DRAW_DEFAULT_ALL : RD::DRAW_CLEAR_COLOR_ALL) | (depth_pre_pass ? RD::DRAW_DEFAULT_ALL : RD::DRAW_CLEAR_DEPTH), c, 0.0f, 0u, p_render_data->render_region); } @@ -2248,7 +2263,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co rp_uniform_set = _setup_render_pass_uniform_set(RENDER_LIST_MOTION, p_render_data, is_multiview, radiance_texture, samplers, opaque_pass_uniform_buffer_index, true); - RenderListParameters render_list_params(render_list[RENDER_LIST_MOTION].elements.ptr(), render_list[RENDER_LIST_MOTION].element_info.ptr(), render_list[RENDER_LIST_MOTION].elements.size(), reverse_cull, PASS_MODE_COLOR, color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization); + RenderListParameters render_list_params(render_list[RENDER_LIST_MOTION].elements.ptr(), render_list[RENDER_LIST_MOTION].element_info.ptr(), render_list[RENDER_LIST_MOTION].elements.size(), reverse_cull, PASS_MODE_COLOR, color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization, !is_reflection_probe); _render_list_with_draw_list(&render_list_params, color_framebuffer); RD::get_singleton()->draw_command_end_label(); @@ -2424,7 +2439,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co transparent_color_pass_flags &= ~uint32_t(COLOR_PASS_FLAG_MOTION_VECTORS); RID alpha_framebuffer = rb_data.is_valid() ? rb_data->get_color_pass_fb(transparent_color_pass_flags) : color_only_framebuffer; - RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), reverse_cull, PASS_MODE_COLOR, transparent_color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization); + RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), reverse_cull, PASS_MODE_COLOR, transparent_color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, base_specialization, !is_reflection_probe); _render_list_with_draw_list(&render_list_params, alpha_framebuffer, RD::DRAW_DEFAULT_ALL, Vector(), 0.0f, 0u, p_render_data->render_region); } @@ -3761,6 +3776,19 @@ RID RenderForwardClustered::_setup_render_pass_uniform_set(RenderListType p_rend u.append_id(texture); uniforms.push_back(u); } +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + { + RD::Uniform u; + u.binding = 37; + u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; + RID instance_buffer = TextureStreaming::get_singleton()->feedback_buffer_get_uniform_rid(); + if (instance_buffer.is_null()) { + instance_buffer = scene_shader.default_material_feedback_buffer; + } + u.append_id(instance_buffer); + uniforms.push_back(u); + } +#endif // MODULE_TEXTURE_STREAMING_ENABLED return UniformSetCacheRD::get_singleton()->get_cache_vec(scene_shader.get_default_shader_rd(is_multiview), RENDER_PASS_UNIFORM_SET, uniforms); } @@ -3967,6 +3995,14 @@ RID RenderForwardClustered::_setup_sdfgi_render_pass_uniform_set(RID p_albedo_te u.append_id(p_geom_facing_texture); uniforms.push_back(u); } + { + RD::Uniform u; + u.binding = 37; + u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; + RID instance_buffer = scene_shader.default_material_feedback_buffer; + u.append_id(instance_buffer); + uniforms.push_back(u); + } if (scene_shader.default_shader_sdfgi_rd.is_null()) { // The variant for SDF from the default material should only be retrieved when SDFGI is required. @@ -5119,6 +5155,18 @@ RenderForwardClustered::RenderForwardClustered() { defines += "\n#define USE_VERTEX_LIGHTING\n"; } +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + bool texture_streaming = GLOBAL_GET("rendering/textures/streaming/enabled"); + if (texture_streaming) { + defines += "\n#define TEXTURE_STREAMING\n"; + + bool prepass_enabled = GLOBAL_GET("rendering/driver/depth_prepass/enable"); + if (prepass_enabled) { + defines += "\n#define TEXTURE_STREAMING_DEPTH_PREPASS\n"; + } + } +#endif + bool specular_occlusion = GLOBAL_GET("rendering/reflections/specular_occlusion/enabled"); if (!specular_occlusion) { defines += "\n#define SPECULAR_OCCLUSION_DISABLED\n"; diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.h b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.h index 7dec40d892d7..461819dea848 100644 --- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.h +++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.h @@ -237,8 +237,9 @@ class RenderForwardClustered : public RendererSceneRenderRD { uint32_t element_offset = 0; bool use_directional_soft_shadow = false; SceneShaderForwardClustered::ShaderSpecialization base_specialization = {}; + bool use_material_feedback = false; - RenderListParameters(GeometryInstanceSurfaceDataCache **p_elements, RenderElementInfo *p_element_info, int p_element_count, bool p_reverse_cull, PassMode p_pass_mode, uint32_t p_color_pass_flags, bool p_no_gi, bool p_use_directional_soft_shadows, RID p_render_pass_uniform_set, bool p_force_wireframe = false, const Vector2 &p_uv_offset = Vector2(), float p_lod_distance_multiplier = 0.0, float p_screen_mesh_lod_threshold = 0.0, uint32_t p_view_count = 1, uint32_t p_element_offset = 0, SceneShaderForwardClustered::ShaderSpecialization p_base_specialization = {}) { + RenderListParameters(GeometryInstanceSurfaceDataCache **p_elements, RenderElementInfo *p_element_info, int p_element_count, bool p_reverse_cull, PassMode p_pass_mode, uint32_t p_color_pass_flags, bool p_no_gi, bool p_use_directional_soft_shadows, RID p_render_pass_uniform_set, bool p_force_wireframe = false, const Vector2 &p_uv_offset = Vector2(), float p_lod_distance_multiplier = 0.0, float p_screen_mesh_lod_threshold = 0.0, uint32_t p_view_count = 1, uint32_t p_element_offset = 0, SceneShaderForwardClustered::ShaderSpecialization p_base_specialization = {}, bool p_use_material_feedback = false) { elements = p_elements; element_info = p_element_info; element_count = p_element_count; @@ -255,6 +256,7 @@ class RenderForwardClustered : public RendererSceneRenderRD { element_offset = p_element_offset; use_directional_soft_shadow = p_use_directional_soft_shadows; base_specialization = p_base_specialization; + use_material_feedback = p_use_material_feedback; } }; @@ -334,7 +336,8 @@ class RenderForwardClustered : public RendererSceneRenderRD { struct InstanceData { float transform[12]; float compressed_aabb_position[4]; - float compressed_aabb_size[4]; + float compressed_aabb_size[3]; + uint32_t material_feedback_index; float uv_scale[4]; uint32_t flags; uint32_t instance_uniforms_ofs; //base offset in global buffer for instance variables diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp index 856f1001b6a8..eda7d9fba6ce 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp @@ -405,7 +405,7 @@ void SceneShaderForwardClustered::ShaderData::_create_pipeline(PipelineKey p_pip depth_stencil_state.back_op = op; } - bool depth_pre_pass_enabled = bool(GLOBAL_GET_CACHED(bool, "rendering/driver/depth_prepass/enable")); + const bool depth_pre_pass_enabled = SceneShaderForwardClustered::singleton->depth_prepass_enabled; RD::RenderPrimitive primitive_rd_table[RSE::PRIMITIVE_MAX] = { RD::RENDER_PRIMITIVE_POINTS, @@ -626,6 +626,7 @@ SceneShaderForwardClustered::~SceneShaderForwardClustered() { RD::get_singleton()->free_rid(default_vec4_xform_buffer); RD::get_singleton()->free_rid(shadow_sampler); + RD::get_singleton()->free_rid(default_material_feedback_buffer); material_storage->shader_free(overdraw_material_shader); material_storage->shader_free(default_shader); @@ -641,6 +642,8 @@ void SceneShaderForwardClustered::init(const String p_defines) { emulate_point_size = !RD::get_singleton()->has_feature(RD::SUPPORTS_POINT_SIZE); + depth_prepass_enabled = GLOBAL_GET("rendering/driver/depth_prepass/enable"); + { Vector shader_versions; for (uint32_t ubershader = 0; ubershader < 2; ubershader++) { @@ -719,6 +722,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.renames["BINORMAL"] = "binormal"; actions.renames["POSITION"] = "position"; actions.renames["UV"] = "uv_interp"; + actions.renames["STREAMING_UV"] = "streaming_uv"; actions.renames["UV2"] = "uv2_interp"; actions.renames["COLOR"] = "color_interp"; actions.renames["POINT_SIZE"] = "point_size"; @@ -820,6 +824,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.usage_defines["AO"] = "#define AO_USED\n"; actions.usage_defines["AO_LIGHT_AFFECT"] = "#define AO_USED\n"; actions.usage_defines["UV"] = "#define UV_USED\n"; + actions.usage_defines["STREAMING_UV"] = "#define STREAMING_UV_USED\n"; actions.usage_defines["UV2"] = "#define UV2_USED\n"; actions.usage_defines["BONE_INDICES"] = "#define BONES_USED\n"; actions.usage_defines["BONE_WEIGHTS"] = "#define WEIGHTS_USED\n"; @@ -859,6 +864,9 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.usage_defines["POINT_SIZE"] = "#define POINT_SIZE_USED\n"; actions.usage_defines["POINT_COORD"] = "#define POINT_COORD_USED\n"; + actions.usage_defines["DISCARD"] = "#define DISCARD_USED\n"; + actions.usage_defines["DEPTH"] = "#define DEPTH_USED\n"; + actions.render_mode_defines["skip_vertex_transform"] = "#define SKIP_TRANSFORM_USED\n"; actions.render_mode_defines["world_vertex_coords"] = "#define VERTEX_WORLD_COORDS_USED\n"; actions.render_mode_defines["ensure_correct_normals"] = "#define ENSURE_CORRECT_NORMALS\n"; @@ -867,6 +875,12 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.render_mode_defines["particle_trails"] = "#define USE_PARTICLE_TRAILS\n"; actions.render_mode_defines["depth_prepass_alpha"] = "#define USE_OPAQUE_PREPASS\n"; + actions.render_mode_defines["depth_draw_never"] = "#define DEPTH_DRAW_NEVER_USED\n"; + actions.render_mode_defines["depth_draw_always"] = "#define DEPTH_DRAW_ALWAYS_USED\n"; + actions.render_mode_defines["depth_test_disabled"] = "#define DEPTH_TEST_DISABLED_USED\n"; + actions.stencil_mode_defines["write"] = "#define STENCIL_WRITE_USED\n"; + actions.stencil_mode_defines["write_depth_fail"] = "#define STENCIL_WRITE_USED\n"; + bool force_lambert = GLOBAL_GET("rendering/shading/overrides/force_lambert_over_burley"); if (!force_lambert) { @@ -993,6 +1007,7 @@ void fragment() { { default_vec4_xform_buffer = RD::get_singleton()->storage_buffer_create(256); + default_material_feedback_buffer = RD::get_singleton()->storage_buffer_create(256); Vector uniforms; RD::Uniform u; u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h index 5e1af939eb9f..838fc08233ac 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h @@ -128,6 +128,7 @@ class SceneShaderForwardClustered { uint32_t multimesh_has_custom_data : 1; uint32_t fog_use_legacy_blending : 1; uint32_t cluster_has_area_light : 1; + uint32_t material_feedback : 1; }; }; @@ -347,6 +348,7 @@ class SceneShaderForwardClustered { SceneForwardClusteredShaderRD shader; ShaderCompiler compiler; bool emulate_point_size = false; + bool depth_prepass_enabled = false; RID default_shader; RID default_material; @@ -360,6 +362,7 @@ class SceneShaderForwardClustered { RID default_vec4_xform_buffer; RID default_vec4_xform_uniform_set; + RID default_material_feedback_buffer; RID shadow_sampler; diff --git a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp index 732ec9bb5564..e5262bc07b23 100644 --- a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp @@ -46,6 +46,11 @@ #include "servers/xr/xr_server.h" #endif +#include "modules/modules_enabled.gen.h" +#ifdef MODULE_TEXTURE_STREAMING_ENABLED +#include "modules/texture_streaming/texture_streaming.h" +#endif + #define PRELOAD_PIPELINES_ON_SURFACE_CACHE_CONSTRUCTION 1 using namespace RendererSceneRenderImplementation; @@ -724,6 +729,20 @@ RID RenderForwardMobile::_setup_render_pass_uniform_set(RenderListType p_render_ p_samplers.append_uniforms(uniforms, 13); +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + { + RD::Uniform u; + u.binding = 25; + u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; + RID instance_buffer = TextureStreaming::get_singleton()->feedback_buffer_get_uniform_rid(); + if (instance_buffer.is_null()) { + instance_buffer = scene_shader.default_material_feedback_buffer; + } + u.append_id(instance_buffer); + uniforms.push_back(u); + } +#endif // MODULE_TEXTURE_STREAMING_ENABLED + return UniformSetCacheRD::get_singleton()->get_cache_vec(scene_shader.get_default_shader_rd(is_multiview), RENDER_PASS_UNIFORM_SET, uniforms); } @@ -1270,7 +1289,7 @@ void RenderForwardMobile::_render_scene(RenderDataRD *p_render_data, const Color } if (render_list[RENDER_LIST_OPAQUE].elements.size() > 0) { - RenderListParameters render_list_params(render_list[RENDER_LIST_OPAQUE].elements.ptr(), render_list[RENDER_LIST_OPAQUE].element_info.ptr(), render_list[RENDER_LIST_OPAQUE].elements.size(), reverse_cull, PASS_MODE_COLOR, rp_uniform_set, base_specialization, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count); + RenderListParameters render_list_params(render_list[RENDER_LIST_OPAQUE].elements.ptr(), render_list[RENDER_LIST_OPAQUE].element_info.ptr(), render_list[RENDER_LIST_OPAQUE].elements.size(), reverse_cull, PASS_MODE_COLOR, rp_uniform_set, base_specialization, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, !is_reflection_probe); render_list_params.framebuffer_format = fb_format; render_list_params.subpass = RD::get_singleton()->draw_list_get_current_pass(); // Should now always be 0. @@ -1297,7 +1316,7 @@ void RenderForwardMobile::_render_scene(RenderDataRD *p_render_data, const Color rp_uniform_set = _setup_render_pass_uniform_set(RENDER_LIST_ALPHA, p_render_data, is_multiview, radiance_texture, samplers, true); - RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), reverse_cull, PASS_MODE_COLOR_TRANSPARENT, rp_uniform_set, base_specialization, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count); + RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), reverse_cull, PASS_MODE_COLOR_TRANSPARENT, rp_uniform_set, base_specialization, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, !is_reflection_probe); render_list_params.framebuffer_format = fb_format; render_list_params.subpass = RD::get_singleton()->draw_list_get_current_pass(); // Should now always be 0. @@ -1366,7 +1385,7 @@ void RenderForwardMobile::_render_scene(RenderDataRD *p_render_data, const Color // this may be needed if we re-introduced steps that change info, not sure which do so in the previous implementation //_setup_environment(p_render_data, is_reflection_probe, screen_size, screen_size, p_default_bg_color, false); - RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), reverse_cull, PASS_MODE_COLOR, rp_uniform_set, base_specialization, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count); + RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), reverse_cull, PASS_MODE_COLOR, rp_uniform_set, base_specialization, get_debug_draw_mode() == RSE::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, !is_reflection_probe); render_list_params.framebuffer_format = fb_format; render_list_params.subpass = RD::get_singleton()->draw_list_get_current_pass(); // Should now always be 0. @@ -2148,7 +2167,15 @@ void RenderForwardMobile::_fill_instance_data(RenderListType p_render_list, uint fill_push_constant_instance_indices(&instance_data, inst); + uint32_t material_feedback_index = UINT32_MAX; +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + if (surface->material->material_feedback_rid.is_valid()) { + material_feedback_index = TextureStreaming::get_singleton()->feedback_buffer_material_index(surface->material->material_feedback_rid); + } +#endif + instance_data.set_compressed_aabb(surface_aabb); + instance_data.material_feedback_index = material_feedback_index; instance_data.set_uv_scale(uv_scale); scene_state.curr_gpu_ptr[p_render_list][i + p_offset] = instance_data; @@ -2459,6 +2486,7 @@ void RenderForwardMobile::_render_list_template(RenderingDevice::DrawListID p_dr pipeline_specialization.multimesh_format_2d = bool(inst->flags_cache & INSTANCE_DATA_FLAG_MULTIMESH_FORMAT_2D); pipeline_specialization.multimesh_has_color = bool(inst->flags_cache & INSTANCE_DATA_FLAG_MULTIMESH_HAS_COLOR); pipeline_specialization.multimesh_has_custom_data = bool(inst->flags_cache & INSTANCE_DATA_FLAG_MULTIMESH_HAS_CUSTOM_DATA); + pipeline_specialization.material_feedback = p_params->use_material_feedback && surf->material->material_feedback_rid.is_valid(); SceneState::PushConstant push_constant; push_constant.base_index = i + p_params->element_offset; @@ -3610,6 +3638,14 @@ RenderForwardMobile::RenderForwardMobile() { defines += "\n#define USE_DOUBLE_PRECISION \n"; } #endif + { +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + bool texture_streaming = GLOBAL_GET("rendering/textures/streaming/enabled"); + if (texture_streaming) { + defines += "\n#define TEXTURE_STREAMING\n"; + } +#endif + } scene_shader.init(defines); diff --git a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.h b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.h index 1d7bd719a580..9badad6306f9 100644 --- a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.h +++ b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.h @@ -139,8 +139,9 @@ class RenderForwardMobile : public RendererSceneRenderRD { RD::FramebufferFormatID framebuffer_format = 0; uint32_t element_offset = 0; uint32_t subpass = 0; + bool use_material_feedback = false; - RenderListParameters(GeometryInstanceSurfaceDataCache **p_elements, RenderElementInfo *p_element_info, int p_element_count, bool p_reverse_cull, PassMode p_pass_mode, RID p_render_pass_uniform_set, SceneShaderForwardMobile::ShaderSpecialization p_base_specialization, bool p_force_wireframe = false, const Vector2 &p_uv_offset = Vector2(), float p_lod_distance_multiplier = 0.0, float p_screen_mesh_lod_threshold = 0.0, uint32_t p_view_count = 1, uint32_t p_element_offset = 0) { + RenderListParameters(GeometryInstanceSurfaceDataCache **p_elements, RenderElementInfo *p_element_info, int p_element_count, bool p_reverse_cull, PassMode p_pass_mode, RID p_render_pass_uniform_set, SceneShaderForwardMobile::ShaderSpecialization p_base_specialization, bool p_force_wireframe = false, const Vector2 &p_uv_offset = Vector2(), float p_lod_distance_multiplier = 0.0, float p_screen_mesh_lod_threshold = 0.0, uint32_t p_view_count = 1, uint32_t p_element_offset = 0, bool p_use_material_feedback = false) { elements = p_elements; element_info = p_element_info; element_count = p_element_count; @@ -155,6 +156,7 @@ class RenderForwardMobile : public RendererSceneRenderRD { screen_mesh_lod_threshold = p_screen_mesh_lod_threshold; element_offset = p_element_offset; base_specialization = p_base_specialization; + use_material_feedback = p_use_material_feedback; } }; @@ -217,7 +219,8 @@ class RenderForwardMobile : public RendererSceneRenderRD { struct InstanceData { float transform[12]; float compressed_aabb_position[4]; - float compressed_aabb_size[4]; + float compressed_aabb_size[3]; + uint32_t material_feedback_index; // Index into the material feedback buffer. float uv_scale[4]; uint32_t flags; uint32_t instance_uniforms_ofs; // Base offset in global buffer for instance variables. diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp index bd0b6ab6d960..5c28a4d3aec9 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp @@ -659,6 +659,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.renames["BINORMAL"] = "binormal_highp"; actions.renames["POSITION"] = "position"; actions.renames["UV"] = "uv_interp"; + actions.renames["STREAMING_UV"] = "streaming_uv"; actions.renames["UV2"] = "uv2_interp"; actions.renames["COLOR"] = "color_highp"; actions.renames["POINT_SIZE"] = "point_size"; @@ -760,6 +761,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.usage_defines["AO"] = "#define AO_USED\n"; actions.usage_defines["AO_LIGHT_AFFECT"] = "#define AO_USED\n"; actions.usage_defines["UV"] = "#define UV_USED\n"; + actions.usage_defines["STREAMING_UV"] = "#define STREAMING_UV_USED\n"; actions.usage_defines["UV2"] = "#define UV2_USED\n"; actions.usage_defines["BONE_INDICES"] = "#define BONES_USED\n"; actions.usage_defines["BONE_WEIGHTS"] = "#define WEIGHTS_USED\n"; @@ -799,6 +801,9 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.usage_defines["POINT_SIZE"] = "#define POINT_SIZE_USED\n"; actions.usage_defines["POINT_COORD"] = "#define POINT_COORD_USED\n"; + actions.usage_defines["DISCARD"] = "#define DISCARD_USED\n"; + actions.usage_defines["DEPTH"] = "#define DEPTH_USED\n"; + actions.render_mode_defines["skip_vertex_transform"] = "#define SKIP_TRANSFORM_USED\n"; actions.render_mode_defines["world_vertex_coords"] = "#define VERTEX_WORLD_COORDS_USED\n"; actions.render_mode_defines["ensure_correct_normals"] = "#define ENSURE_CORRECT_NORMALS\n"; @@ -807,6 +812,12 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.render_mode_defines["particle_trails"] = "#define USE_PARTICLE_TRAILS\n"; actions.render_mode_defines["depth_prepass_alpha"] = "#define USE_OPAQUE_PREPASS\n"; + actions.render_mode_defines["depth_draw_never"] = "#define DEPTH_DRAW_NEVER_USED\n"; + actions.render_mode_defines["depth_draw_always"] = "#define DEPTH_DRAW_ALWAYS_USED\n"; + actions.render_mode_defines["depth_test_disabled"] = "#define DEPTH_TEST_DISABLED_USED\n"; + actions.stencil_mode_defines["write"] = "#define STENCIL_WRITE_USED\n"; + actions.stencil_mode_defines["write_depth_fail"] = "#define STENCIL_WRITE_USED\n"; + bool force_lambert = GLOBAL_GET("rendering/shading/overrides/force_lambert_over_burley"); if (!force_lambert) { actions.render_mode_defines["diffuse_burley"] = "#define DIFFUSE_BURLEY\n"; @@ -934,6 +945,7 @@ void fragment() { { default_vec4_xform_buffer = RD::get_singleton()->storage_buffer_create(256); + default_material_feedback_buffer = RD::get_singleton()->storage_buffer_create(256); Vector uniforms; RD::Uniform u; u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; @@ -1012,6 +1024,7 @@ SceneShaderForwardMobile::~SceneShaderForwardMobile() { RendererRD::MaterialStorage *material_storage = RendererRD::MaterialStorage::get_singleton(); RD::get_singleton()->free_rid(default_vec4_xform_buffer); + RD::get_singleton()->free_rid(default_material_feedback_buffer); RD::get_singleton()->free_rid(shadow_sampler); material_storage->shader_free(overdraw_material_shader); diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h index bef0e806f4a4..bf2bea1454ae 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h @@ -109,7 +109,7 @@ class SceneShaderForwardMobile { uint32_t scene_use_ambient_cubemap : 1; uint32_t scene_use_reflection_cubemap : 1; uint32_t scene_roughness_limiter_enabled : 1; - uint32_t padding_0 : 1; + uint32_t material_feedback : 1; uint32_t soft_shadow_samples : 6; uint32_t penumbra_shadow_samples : 6; @@ -361,6 +361,7 @@ class SceneShaderForwardMobile { RID default_vec4_xform_buffer; RID default_vec4_xform_uniform_set; + RID default_material_feedback_buffer; RID shadow_sampler; diff --git a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl index 1c064975efa4..0465db495254 100644 --- a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl @@ -880,6 +880,25 @@ void main() { /* Varyings */ +#if defined(TEXTURE_STREAMING) && !defined(MODE_RENDER_DEPTH) && (defined(UV_USED) || defined(STREAMING_UV_USED)) +// Since material feedback writes to a ssbo buffer, early fragment tests likely get disabled by the +// driver so unless we want really bad performance, we need to force enable it again. +// +// To Early-Z, or Not To Early-Z +// - https://therealmjp.github.io/posts/to-earlyz-or-not-to-earlyz/#uavsstorage-texturesstorage-buffers +#if defined(ALPHA_SCISSOR_USED) || defined(ALPHA_HASH_USED) || defined(ENABLE_CLIP_ALPHA) || defined(UBERSHADER) || defined(DISCARD_USED) +#define TEXTURE_STREAMING_MAY_DISCARD +#endif + +#if defined(DEPTH_TEST_DISABLED_USED) || defined(DEPTH_DRAW_NEVER_USED) || (defined(TEXTURE_STREAMING_DEPTH_PREPASS) && !defined(DEPTH_DRAW_ALWAYS_USED)) +#define TEXTURE_STREAMING_NO_DEPTH_WRITE +#endif + +#if !defined(DEPTH_USED) && (!defined(TEXTURE_STREAMING_MAY_DISCARD) || (defined(TEXTURE_STREAMING_NO_DEPTH_WRITE) && !defined(STENCIL_WRITE_USED))) +layout(early_fragment_tests) in; +#endif +#endif // TEXTURE_STREAMING + layout(location = 0) in vec3 vertex_interp; #ifdef NORMAL_USED @@ -894,6 +913,10 @@ layout(location = 2) in vec4 color_interp; layout(location = 3) in vec2 uv_interp; #endif +#if defined(TEXTURE_STREAMING) +vec2 streaming_uv; +#endif + #if defined(UV2_USED) || defined(USE_LIGHTMAP) layout(location = 4) in vec2 uv2_interp; #endif @@ -1391,6 +1414,51 @@ void fragment_shader(in SceneData scene_data) { #endif // MODE_RENDER_MATERIAL #endif // ALPHA_SCISSOR_USED +#if defined(UV_USED) || defined(STREAMING_UV_USED) +#if defined(TEXTURE_STREAMING) +#if !defined(MODE_RENDER_DEPTH) + if (sc_material_feedback()) { +// When STREAMING_UV_USED is not used just use normal UVs. +#if !defined(STREAMING_UV_USED) + streaming_uv = uv_interp; +#endif + // Instance has materials which require feedback. + vec2 uv_dx = dFdx(streaming_uv); + vec2 uv_dy = dFdy(streaming_uv); + + if (!gl_HelperInvocation) { + // Calculate the mip level needed for the current fragment based on UV derivatives. + float px_sq = dot(uv_dx, uv_dx); + float py_sq = dot(uv_dy, uv_dy); + float min_sq = min(px_sq, py_sq); + float max_sq = max(px_sq, py_sq); + + // Anisotropic filtering allows using the mip level of the minor axis (min_sq), + // but limited by the max anisotropy (usually 16x). + // If the anisotropy ratio exceeds 16, we are forced to use a lower res mip. + const float MAX_ANISOTROPY = 16.0; + float lod_sq = max(min_sq, max_sq / (MAX_ANISOTROPY * MAX_ANISOTROPY)); + + // Bitwise NOT inverts the ordering so that smaller lod_sq (higher quality) + // maps to larger uint values, allowing atomicMax with a 0-cleared buffer. + uint required_mip = ~floatBitsToUint(lod_sq); + + // Reduce atomic contention using subgroup operations. + // Find maximum inverted mip level across all invocations in the subgroup, then only + // one invocation performs the atomic write. + // Right now this assumes all invocations will have the same instance index. + // If that is not true then probably need a subgroupAllEqual check first + fallback. + uint subgroup_max_mip = subgroupMax(required_mip); + if (subgroupElect()) { + const uint material_feedback_index = instances.data[instance_index].material_feedback_index; + atomicMax(material_feedback.data[material_feedback_index], subgroup_max_mip); + } + } + } +#endif // MODE_RENDER_DEPTH +#endif // TEXTURE_STREAMING +#endif // UV_USED || STREAMING_UV_USED + // alpha hash can be used in unison with alpha antialiasing #ifdef ALPHA_HASH_USED vec3 object_pos = (inverse(read_model_matrix) * inv_view_matrix * vec4(vertex, 1.0)).xyz; diff --git a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered_inc.glsl b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered_inc.glsl index 1362d1065a51..994ab247497d 100644 --- a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered_inc.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered_inc.glsl @@ -146,6 +146,10 @@ bool sc_cluster_has_area_light() { return ((sc_packed_1() >> 5) & 1U) != 0; } +bool sc_material_feedback() { + return ((sc_packed_1() >> 6) & 1U) != 0; +} + float sc_luminance_multiplier() { // Not used in clustered renderer but we share some code with the mobile renderer that requires this. return 1.0; @@ -352,7 +356,8 @@ implementation_data_block; struct InstanceData { mat3x4 transform; vec4 compressed_aabb_position_pad; // Only .xyz is used. .w is padding. - vec4 compressed_aabb_size_pad; // Only .xyz is used. .w is padding. + vec3 compressed_aabb_size_pad; // Only .xyz is used. .w is padding. + uint material_feedback_index; // Index into the material feedback buffer. vec4 uv_scale; uint flags; uint instance_uniforms_ofs; //base offset in global buffer for instance variables @@ -503,6 +508,14 @@ vec3 get_energy_compensation(vec3 f0, float env) { return 1.0 + f0 * (1.0 / env - 1.0); } +#ifdef TEXTURE_STREAMING +// Texture streaming material feedback buffer access +layout(set = 1, binding = 37, std430) buffer restrict MaterialFeedbackBuffer { + uint data[]; +} +material_feedback; +#endif + /* Set 2 Skeleton & Instancing (can change per item) */ layout(set = 2, binding = 0, std430) restrict readonly buffer Transforms { diff --git a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl index b3834ab0e822..54c0cee510f0 100644 --- a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl @@ -846,6 +846,32 @@ void main() { /* Varyings */ +#if defined(TEXTURE_STREAMING) && !defined(MODE_RENDER_DEPTH) && (defined(UV_USED) || defined(STREAMING_UV_USED)) +// When texture streaming is enabled subgroupElect is needed to properly handle texture feedback. +#extension GL_KHR_shader_subgroup_basic : enable + +#extension GL_KHR_shader_subgroup_arithmetic : enable + +// Since material feedback writes to a ssbo buffer, early fragment tests likely get disabled by the +// driver so unless we want bad performance, we need to force enable it again when we can. +// +// To Early-Z, or Not To Early-Z +// - https://therealmjp.github.io/posts/to-earlyz-or-not-to-earlyz/#uavsstorage-texturesstorage-buffers +#if defined(ALPHA_SCISSOR_USED) || defined(ALPHA_HASH_USED) || defined(ENABLE_CLIP_ALPHA) || defined(UBERSHADER) || defined(DISCARD_USED) +#define TEXTURE_STREAMING_MAY_DISCARD +#endif + +#if defined(DEPTH_TEST_DISABLED_USED) || defined(DEPTH_DRAW_NEVER_USED) +#define TEXTURE_STREAMING_NO_DEPTH_WRITE +#endif + +#if !defined(DEPTH_USED) && (!defined(TEXTURE_STREAMING_MAY_DISCARD) || (defined(TEXTURE_STREAMING_NO_DEPTH_WRITE) && !defined(STENCIL_WRITE_USED))) +layout(early_fragment_tests) in; +#define EARLY_Z_ON +#endif // early fragment tests are safe + +#endif // TEXTURE_STREAMING + // All interpolators are intentionally kept at full precision as storageInputOutput16 is not // checked for support. Devices with Adreno GPUs don't usually support this capability. @@ -863,6 +889,10 @@ layout(location = 2) in vec4 color_interp; layout(location = 3) in vec2 uv_interp; #endif +#if defined(TEXTURE_STREAMING) +vec2 streaming_uv; +#endif + #if defined(UV2_USED) || defined(USE_LIGHTMAP) layout(location = 4) in vec2 uv2_interp; #endif @@ -1367,6 +1397,46 @@ void main() { #endif // MODE_RENDER_MATERIAL #endif // ALPHA_SCISSOR_USED +#if defined(TEXTURE_STREAMING) && !defined(MODE_RENDER_DEPTH) && (defined(UV_USED) || defined(STREAMING_UV_USED)) + if (sc_material_feedback()) { + // Instance has materials which require feedback. +#if !defined(STREAMING_UV_USED) + vec2 streaming_uv = uv_interp; +#endif + vec2 uv_dx = dFdx(streaming_uv); + vec2 uv_dy = dFdy(streaming_uv); + + if (!gl_HelperInvocation) { + // Calculate the mip level needed for the current fragment based on UV derivatives. + float px_sq = dot(uv_dx, uv_dx); + float py_sq = dot(uv_dy, uv_dy); + float min_sq = min(px_sq, py_sq); + float max_sq = max(px_sq, py_sq); + + // Anisotropic filtering allows using the mip level of the minor axis (min_sq), + // but limited by the max anisotropy (usually 16x). + // If the anisotropy ratio exceeds 16, we are forced to use a lower res mip. + const float MAX_ANISOTROPY = 16.0; + float lod_sq = max(min_sq, max_sq / (MAX_ANISOTROPY * MAX_ANISOTROPY)); + + // Bitwise NOT inverts the ordering so that smaller lod_sq (higher quality) + // maps to larger uint values, allowing atomicMax with a 0-cleared buffer. + uint required_mip = ~floatBitsToUint(lod_sq); + + // Reduce atomic contention using subgroup operations. + // Find maximum inverted mip level across all invocations in the subgroup, then only + // one invocation performs the atomic write. + // Right now this assumes all invocations will have the same instance index. + // If that is not true then probably need a subgroupAllEqual check first + fallback. + uint subgroup_max_mip = subgroupMax(required_mip); + if (subgroupElect()) { + const uint material_feedback_index = instances.data[draw_call.instance_index].material_feedback_index; + atomicMax(material_feedback.data[material_feedback_index], subgroup_max_mip); + } + } + } +#endif //TEXTURE_STREAMING + // alpha hash can be used in unison with alpha antialiasing #ifdef ALPHA_HASH_USED vec3 object_pos = (inverse(read_model_matrix) * inv_view_matrix * vec4(vertex, 1.0)).xyz; diff --git a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile_inc.glsl b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile_inc.glsl index ee17aad67658..fcc4f3c44b8a 100644 --- a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile_inc.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile_inc.glsl @@ -149,6 +149,10 @@ bool sc_scene_roughness_limiter_enabled() { return ((sc_packed_0() >> 18) & 1U) != 0; } +bool sc_material_feedback() { + return ((sc_packed_0() >> 19) & 1U) != 0; +} + uint sc_soft_shadow_samples() { return (sc_packed_0() >> 20) & 63U; } @@ -344,7 +348,8 @@ scene_data_block; struct InstanceData { highp mat3x4 transform; vec4 compressed_aabb_position_pad; // Only .xyz is used. .w is padding. - vec4 compressed_aabb_size_pad; // Only .xyz is used. .w is padding. + vec3 compressed_aabb_size_pad; // Only .xyz is used. + uint material_feedback_index; // Index into the material feedback buffer. vec4 uv_scale; uint flags; uint instance_uniforms_ofs; // Base offset in global buffer for instance variables. @@ -416,6 +421,14 @@ layout(set = 1, binding = 13 + 9) uniform sampler SAMPLER_LINEAR_WITH_MIPMAPS_RE layout(set = 1, binding = 13 + 10) uniform sampler SAMPLER_NEAREST_WITH_MIPMAPS_ANISOTROPIC_REPEAT; layout(set = 1, binding = 13 + 11) uniform sampler SAMPLER_LINEAR_WITH_MIPMAPS_ANISOTROPIC_REPEAT; +#ifdef TEXTURE_STREAMING +// Texture streaming material feedback buffer access +layout(set = 1, binding = 25, std430) buffer restrict MaterialFeedbackBuffer { + uint data[]; +} +material_feedback; +#endif + /* Set 2 Skeleton & Instancing (can change per item) */ layout(set = 2, binding = 0, std430) restrict readonly buffer Transforms { diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp index d8b0a5cf8b3c..3a74458e1456 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp @@ -40,6 +40,12 @@ #include "servers/rendering/renderer_rd/storage_rd/texture_storage.h" #include "servers/rendering/storage/variant_converters.h" +#include "modules/modules_enabled.gen.h" + +#ifdef MODULE_TEXTURE_STREAMING_ENABLED +#include "modules/texture_streaming/texture_streaming.h" +#endif + using namespace RendererRD; /////////////////////////////////////////////////////////////////////////// @@ -857,6 +863,14 @@ MaterialStorage::MaterialData::~MaterialData() { material_storage->global_shader_uniforms.materials_using_texture.erase(global_texture_E); } +#ifdef MODULE_TEXTURE_STREAMING_ENABLED + if (material_feedback_rid.is_valid()) { + Vector empty_textures; + TextureStreaming::get_singleton()->material_set_textures(material_feedback_rid, empty_textures); + material_feedback_rid = RID(); + } +#endif + for (int i = 0; i < 2; i++) { if (uniform_buffer[i].is_valid()) { RD::get_singleton()->free_rid(uniform_buffer[i]); @@ -875,6 +889,7 @@ void MaterialStorage::MaterialData::update_textures(const HashMap material_feedback_textures; for (int i = 0, k = 0; i < p_texture_uniforms.size(); i++) { const StringName &uniform_name = p_texture_uniforms[i].name; @@ -983,6 +998,9 @@ void MaterialStorage::MaterialData::update_textures(const HashMaprd_texture_srgb.is_valid()) ? tex->rd_texture_srgb : tex->rd_texture; + if (tex->streaming_state.is_valid()) { + material_feedback_textures.push_back(tex->streaming_state); + } #ifdef TOOLS_ENABLED if (tex->detect_3d_callback && p_3d_material) { tex->detect_3d_callback(tex->detect_3d_callback_ud); @@ -1019,6 +1037,13 @@ void MaterialStorage::MaterialData::update_textures(const HashMapmaterial_set_textures(material_feedback_rid, material_feedback_textures); + } +#endif + { //for textures no longer used, unregister them List to_delete; diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.h b/servers/rendering/renderer_rd/storage_rd/material_storage.h index 46c7352aff9c..1d2cec36ee24 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.h +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.h @@ -105,6 +105,7 @@ class MaterialStorage : public RendererMaterialStorage { //to be used internally by update_parameters, in the most common configuration of material parameters bool update_parameters_uniform_set(const HashMap &p_parameters, bool p_uniform_dirty, bool p_textures_dirty, const HashMap &p_uniforms, const uint32_t *p_uniform_offsets, const Vector &p_texture_uniforms, const HashMap> &p_default_texture_params, uint32_t p_ubo_size, RID &r_uniform_set, RID p_shader, uint32_t p_shader_uniform_set, bool p_use_linear_color, bool p_3d_material); void free_parameters_uniform_set(RID p_uniform_set); + RID material_feedback_rid; private: friend class MaterialStorage; @@ -524,6 +525,10 @@ class MaterialStorage : public RendererMaterialStorage { return material->data; } } + + _FORCE_INLINE_ uint32_t num_materials() const { + return material_owner.get_rid_count(); + } }; } // namespace RendererRD diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp index b78f160837a6..dd4e9a0287b0 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp @@ -2050,8 +2050,12 @@ void TextureStorage::texture_replace(RID p_texture, RID p_by_texture) { Vector proxies_to_update = tex->proxies; Vector proxies_to_redirect = by_tex->proxies; + RID streaming_state = tex->streaming_state; + *tex = *by_tex; + tex->streaming_state = streaming_state; // restore streaming state + tex->proxies = proxies_to_update; //restore proxies, so they can be updated if (tex->canvas_texture) { @@ -2071,6 +2075,64 @@ void TextureStorage::texture_replace(RID p_texture, RID p_by_texture) { area_light_atlas_mark_dirty_on_texture(p_texture); } +void TextureStorage::texture_replace_compatible(RID p_texture, RID p_by_texture) { + Texture *tex = texture_owner.get_or_null(p_texture); + ERR_FAIL_NULL(tex); + ERR_FAIL_COND(tex->proxy_to.is_valid()); //can't replace proxy + Texture *by_tex = texture_owner.get_or_null(p_by_texture); + ERR_FAIL_NULL(by_tex); + ERR_FAIL_COND(by_tex->proxy_to.is_valid()); //can't replace proxy + + if (tex == by_tex) { + return; + } + + RID old_rd_texture = tex->rd_texture; + RID old_rd_texture_srgb = tex->rd_texture_srgb; + RID new_rd_texture = by_tex->rd_texture; + RID new_rd_texture_srgb = by_tex->rd_texture_srgb; + + if (tex->canvas_texture) { + memdelete(tex->canvas_texture); + tex->canvas_texture = nullptr; + } + + Vector proxies_to_update = tex->proxies; + Vector proxies_to_redirect = by_tex->proxies; + + *tex = *by_tex; + + tex->proxies = proxies_to_update; //restore proxies, so they can be updated + + if (tex->canvas_texture) { + tex->canvas_texture->diffuse = p_texture; //update + } + + for (int i = 0; i < proxies_to_update.size(); i++) { + texture_proxy_update(proxies_to_update[i], p_texture); + } + for (int i = 0; i < proxies_to_redirect.size(); i++) { + texture_proxy_update(proxies_to_redirect[i], p_texture); + } + + // Replace RD-level textures: patches uniform sets and defers old resources. + if (old_rd_texture_srgb.is_valid() && old_rd_texture_srgb != new_rd_texture_srgb) { + if (new_rd_texture_srgb.is_valid()) { + RD::get_singleton()->texture_replace_rid(old_rd_texture_srgb, new_rd_texture_srgb); + } else { + RD::get_singleton()->free_rid(old_rd_texture_srgb); + } + } + if (old_rd_texture != new_rd_texture) { + RD::get_singleton()->texture_replace_rid(old_rd_texture, new_rd_texture); + } + + //delete last, so proxies can be updated + texture_owner.free(p_by_texture); + + decal_atlas_mark_dirty_on_texture(p_texture); +} + void TextureStorage::texture_set_size_override(RID p_texture, int p_width, int p_height) { Texture *tex = texture_owner.get_or_null(p_texture); ERR_FAIL_NULL(tex); @@ -5361,3 +5423,9 @@ uint32_t TextureStorage::render_target_get_color_usage_bits(bool p_msaa) { return RD::TEXTURE_USAGE_SAMPLING_BIT | RD::TEXTURE_USAGE_COLOR_ATTACHMENT_BIT | RD::TEXTURE_USAGE_CAN_COPY_FROM_BIT | RD::TEXTURE_USAGE_STORAGE_BIT; } } + +void TextureStorage::texture_2d_attach_streaming_state(RID p_texture, RID p_streaming_state) { + Texture *tex = texture_owner.get_or_null(p_texture); + ERR_FAIL_NULL_MSG(tex, "Invalid texture RID."); + tex->streaming_state = p_streaming_state; +} diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.h b/servers/rendering/renderer_rd/storage_rd/texture_storage.h index d9e577de210a..0dd38d2324d6 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.h +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.h @@ -202,6 +202,8 @@ class TextureStorage : public RendererTextureStorage { CanvasTexture *canvas_texture = nullptr; + RID streaming_state; + void cleanup(); }; @@ -622,6 +624,7 @@ class TextureStorage : public RendererTextureStorage { virtual RID texture_drawable_get_default_material() const override; virtual void texture_replace(RID p_texture, RID p_by_texture) override; + virtual void texture_replace_compatible(RID p_texture, RID p_by_texture) override; virtual void texture_set_size_override(RID p_texture, int p_width, int p_height) override; virtual void texture_set_path(RID p_texture, const String &p_path) override; @@ -640,6 +643,7 @@ class TextureStorage : public RendererTextureStorage { virtual Size2 texture_size_with_proxy(RID p_proxy) override; virtual void texture_rd_initialize(RID p_texture, const RID &p_rd_texture, const RSE::TextureLayeredType p_layer_type = RSE::TEXTURE_LAYERED_2D_ARRAY) override; + virtual RID texture_get_rd_texture(RID p_texture, bool p_srgb = false) const override; virtual uint64_t texture_get_native_handle(RID p_texture, bool p_srgb = false) const override; @@ -933,6 +937,8 @@ class TextureStorage : public RendererTextureStorage { static RD::DataFormat render_target_get_color_format(bool p_use_hdr, bool p_srgb); static uint32_t render_target_get_color_usage_bits(bool p_msaa); + + virtual void texture_2d_attach_streaming_state(RID p_texture, RID p_streaming_state) override; }; } // namespace RendererRD diff --git a/servers/rendering/renderer_rd/uniform_set_cache_rd.cpp b/servers/rendering/renderer_rd/uniform_set_cache_rd.cpp index ff4602aa61c4..cc6ff5a8e81e 100644 --- a/servers/rendering/renderer_rd/uniform_set_cache_rd.cpp +++ b/servers/rendering/renderer_rd/uniform_set_cache_rd.cpp @@ -73,6 +73,55 @@ void UniformSetCacheRD::_uniform_set_invalidation_callback(void *p_userdata) { singleton->_invalidate(reinterpret_cast(p_userdata)); } +void UniformSetCacheRD::texture_replaced_in_uniform_set(void *p_cache_userdata, RID p_old_texture, RID p_new_texture) { + if (!p_cache_userdata) { + // Not a cache-managed uniform set, nothing to do. + return; + } + + Cache *found = reinterpret_cast(p_cache_userdata); + + // Remove from old hash bucket. + if (found->prev) { + found->prev->next = found->next; + } else { + uint32_t old_table_idx = found->hash % HASH_TABLE_SIZE; + hash_table[old_table_idx] = found->next; + } + if (found->next) { + found->next->prev = found->prev; + } + + // Patch the uniforms: replace old texture RID with new. + for (uint32_t i = 0; i < found->uniforms.size(); i++) { + RD::Uniform &u = found->uniforms[i]; + uint32_t id_count = u.get_id_count(); + for (uint32_t j = 0; j < id_count; j++) { + if (u.get_id(j) == p_old_texture) { + u.set_id(j, p_new_texture); + } + } + } + + // Recompute hash. + uint32_t h = hash_murmur3_one_64(found->shader.get_id()); + h = hash_murmur3_one_32(found->set, h); + for (uint32_t i = 0; i < found->uniforms.size(); i++) { + h = _hash_uniform(found->uniforms[i], h); + } + h = hash_fmix32(h); + found->hash = h; + + // Insert into new hash bucket. + uint32_t new_table_idx = h % HASH_TABLE_SIZE; + found->prev = nullptr; + found->next = hash_table[new_table_idx]; + if (hash_table[new_table_idx]) { + hash_table[new_table_idx]->prev = found; + } + hash_table[new_table_idx] = found; +} + UniformSetCacheRD::UniformSetCacheRD() { ERR_FAIL_COND(singleton != nullptr); singleton = this; diff --git a/servers/rendering/renderer_rd/uniform_set_cache_rd.h b/servers/rendering/renderer_rd/uniform_set_cache_rd.h index f8f807db52c0..b3fd9c6a733d 100644 --- a/servers/rendering/renderer_rd/uniform_set_cache_rd.h +++ b/servers/rendering/renderer_rd/uniform_set_cache_rd.h @@ -206,6 +206,16 @@ class UniformSetCacheRD : public Object { static RID get_cache_array(RID p_shader, uint32_t p_set, const TypedArray &p_uniforms); + // Re-keys a cache entry after a texture RID was replaced in its uniforms. + // Called by RenderingDevice::texture_replace_rid() to keep the cache consistent. + // p_cache_userdata is the invalidated_callback_userdata from the UniformSet (the Cache*). + void texture_replaced_in_uniform_set(void *p_cache_userdata, RID p_old_texture, RID p_new_texture); + + // Returns true if the given callback is the one used by this cache. + bool is_cache_invalidation_callback(void (*p_callback)(void *)) const { + return p_callback == _uniform_set_invalidation_callback; + } + static UniformSetCacheRD *get_singleton() { return singleton; } UniformSetCacheRD(); diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp index 719cdb249ca4..96a3080d1408 100644 --- a/servers/rendering/rendering_device.cpp +++ b/servers/rendering/rendering_device.cpp @@ -39,6 +39,7 @@ #include "core/os/os.h" #include "core/profiling/profiling.h" #include "core/templates/fixed_vector.h" +#include "servers/rendering/renderer_rd/uniform_set_cache_rd.h" #include "servers/rendering/rendering_device_binds.h" #include "servers/rendering/rendering_shader_container.h" #include "servers/rendering/shader_include_db.h" @@ -221,6 +222,27 @@ void RenderingDevice::_free_dependencies(RID p_id) { } } +void RenderingDevice::_replace_dependency(RID p_dependent, RID p_old_dependency, RID p_new_dependency) { + // Remove the edge: p_old_dependency -> p_dependent. + { + HashSet *set = dependency_map.getptr(p_old_dependency); + if (set) { + set->erase(p_dependent); + } + } + + // Remove the reverse edge: p_dependent -> p_old_dependency. + { + HashSet *set = reverse_dependency_map.getptr(p_dependent); + if (set) { + set->erase(p_old_dependency); + } + } + + // Add the new edge: p_new_dependency -> p_dependent. + _add_dependency(p_dependent, p_new_dependency); +} + /*******************************/ /**** SHADER INFRASTRUCTURE ****/ /*******************************/ @@ -4849,6 +4871,13 @@ RID RenderingDevice::uniform_set_create(const VectorView &p_uniform uniform_set.acceleration_structures = acceleration_structures; uniform_set.shader_set = p_shader_set; uniform_set.shader_id = p_shader; + uniform_set.is_linear_pool = p_linear_pool; + + // Store the original uniforms so the set can be re-created if a texture is replaced. + uniform_set.bound_uniforms.resize(uniform_count); + for (uint32_t i = 0; i < uniform_count; i++) { + uniform_set.bound_uniforms[i] = uniforms[i]; + } RID id = uniform_set_owner.make_rid(uniform_set); #ifdef DEV_ENABLED @@ -7817,6 +7846,128 @@ void RenderingDevice::_free_internal(RID p_id) { frames_pending_resources_for_processing = uint32_t(frames.size()); } +void RenderingDevice::texture_replace_rid(RID p_old_texture, RID p_new_texture) { + _THREAD_SAFE_METHOD_ + ERR_FAIL_COND(p_old_texture == p_new_texture); + + Texture *old_texture = texture_owner.get_or_null(p_old_texture); + ERR_FAIL_NULL(old_texture); + Texture *new_texture = texture_owner.get_or_null(p_new_texture); + ERR_FAIL_NULL(new_texture); + + // We must snapshot the set because we'll be mutating the dependency map. + LocalVector dependent_uniform_sets; + { + HashSet *deps = dependency_map.getptr(p_old_texture); + if (deps) { + for (const RID &dep : *deps) { + if (uniform_set_owner.owns(dep)) { + dependent_uniform_sets.push_back(dep); + } + } + } + } + + // For each dependent uniform set, re-create its driver-level descriptor set + // with the new texture, then queue the old descriptor set for deferred deletion. + for (const RID &us_rid : dependent_uniform_sets) { + UniformSet *us = uniform_set_owner.get_or_null(us_rid); + ERR_CONTINUE(!us); + + if (us->bound_uniforms.is_empty()) { + // This uniform set wasn't created with stored bindings so it can't be patched. + free_rid(us_rid); + continue; + } + + Shader *shader = shader_owner.get_or_null(us->shader_id); + if (!shader || !shader->driver_id) { + // Shader is gone; the uniform set is orphaned. Free like normal. + free_rid(us_rid); + continue; + } + + // Patch the bound uniforms. + bool patched = false; + for (uint32_t i = 0; i < us->bound_uniforms.size(); i++) { + Uniform &u = us->bound_uniforms[i]; + uint32_t id_count = u.get_id_count(); + for (uint32_t j = 0; j < id_count; j++) { + if (u.get_id(j) == p_old_texture) { + u.set_id(j, p_new_texture); + patched = true; + } + } + } + + if (!patched) { + // This set didn't actually reference the old texture. Skip. + continue; + } + + VectorView uniforms_view(us->bound_uniforms.ptr(), us->bound_uniforms.size()); + RID new_us_rid = uniform_set_create(uniforms_view, us->shader_id, us->shader_set, us->is_linear_pool); + + if (new_us_rid.is_null()) { + ERR_PRINT("Failed to re-create uniform set during texture replacement."); + continue; + } + + UniformSet *new_us = uniform_set_owner.get_or_null(new_us_rid); + ERR_CONTINUE(!new_us); + + // Create a temporary UniformSet with only the driver_id so it gets freed. + UniformSet old_us_for_disposal; + old_us_for_disposal.driver_id = us->driver_id; + frames[frame].uniform_sets_to_dispose_of.push_back(old_us_for_disposal); + + // Copy new data in. + us->driver_id = new_us->driver_id; + us->format = new_us->format; + us->attachable_textures = new_us->attachable_textures; + us->draw_trackers = new_us->draw_trackers; + us->draw_trackers_usage = new_us->draw_trackers_usage; + us->untracked_usage = new_us->untracked_usage; + us->shared_textures_to_update = new_us->shared_textures_to_update; + us->pending_clear_textures = new_us->pending_clear_textures; + us->bound_uniforms = new_us->bound_uniforms; + + // Replace old texture dependency with new. + _replace_dependency(us_rid, p_old_texture, p_new_texture); + + // Update the UniformSetCacheRD entry if this set was created through the cache. + UniformSetCacheRD *const uniform_set_cache = UniformSetCacheRD::get_singleton(); + if (uniform_set_cache->is_cache_invalidation_callback(us->invalidated_callback)) { + uniform_set_cache->texture_replaced_in_uniform_set(us->invalidated_callback_userdata, p_old_texture, p_new_texture); + } + + // Since the real driver id is used by the original set, clear the temporary set's driver id. + new_us->driver_id = RDD::UniformSetID(); + + // Remove its dependency entries and free the temporary RID. + HashMap>::Iterator rev_it = reverse_dependency_map.find(new_us_rid); + if (rev_it) { + for (const RID &dep_on : rev_it->value) { + HashSet *fwd = dependency_map.getptr(dep_on); + if (fwd) { + fwd->erase(new_us_rid); + } + } + reverse_dependency_map.remove(rev_it); + } + HashMap>::Iterator fwd_it = dependency_map.find(new_us_rid); + if (fwd_it) { + dependency_map.remove(fwd_it); + } + + uniform_set_owner.free(new_us_rid); + } + + free_rid(p_old_texture); + + frames_pending_resources_for_processing = uint32_t(frames.size()); +} + // The full list of resources that can be named is in the VkObjectType enum. // We just expose the resources that are owned and can be accessed easily. void RenderingDevice::set_resource_name(RID p_id, const String &p_name) { diff --git a/servers/rendering/rendering_device.h b/servers/rendering/rendering_device.h index f99204e6d204..ad6053ec4398 100644 --- a/servers/rendering/rendering_device.h +++ b/servers/rendering/rendering_device.h @@ -125,6 +125,7 @@ class RenderingDevice : public RenderingDeviceCommons { void _add_dependency(RID p_id, RID p_depends_on); void _remove_dependency(RID p_id, RID p_depends_on); void _free_dependencies(RID p_id); + void _replace_dependency(RID p_dependent, RID p_old_dependency, RID p_new_dependency); private: /***************************/ @@ -1172,6 +1173,10 @@ class RenderingDevice : public RenderingDeviceCommons { Vector acceleration_structures; // Used for validation. InvalidationCallback invalidated_callback = nullptr; void *invalidated_callback_userdata = nullptr; + + // Stored for uniform set re-creation during texture replacement. + LocalVector bound_uniforms; + bool is_linear_pool = false; }; RID_Owner uniform_set_owner; @@ -1900,6 +1905,7 @@ class RenderingDevice : public RenderingDeviceCommons { void _set_max_fps(int p_max_fps); void free_rid(RID p_rid); + void texture_replace_rid(RID p_old_texture, RID p_new_texture); #ifndef DISABLE_DEPRECATED [[deprecated("Use `free_rid()` instead.")]] void free(RID p_rid) { free_rid(p_rid); diff --git a/servers/rendering/rendering_server.cpp b/servers/rendering/rendering_server.cpp index 2227dd07dfcd..b36bc72ce73e 100644 --- a/servers/rendering/rendering_server.cpp +++ b/servers/rendering/rendering_server.cpp @@ -2288,6 +2288,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("texture_drawable_get_default_material"), &RenderingServer::texture_drawable_get_default_material); ClassDB::bind_method(D_METHOD("texture_replace", "texture", "by_texture"), &RenderingServer::texture_replace); + ClassDB::bind_method(D_METHOD("texture_replace_compatible", "texture", "by_texture"), &RenderingServer::texture_replace_compatible); ClassDB::bind_method(D_METHOD("texture_set_size_override", "texture", "width", "height"), &RenderingServer::texture_set_size_override); ClassDB::bind_method(D_METHOD("texture_set_path", "texture", "path"), &RenderingServer::texture_set_path); diff --git a/servers/rendering/rendering_server.h b/servers/rendering/rendering_server.h index d208a4432d49..0619f610b12e 100644 --- a/servers/rendering/rendering_server.h +++ b/servers/rendering/rendering_server.h @@ -133,6 +133,7 @@ class RenderingServer : public Object { virtual Vector> texture_3d_get(RID p_texture) const = 0; virtual void texture_replace(RID p_texture, RID p_by_texture) = 0; + virtual void texture_replace_compatible(RID p_texture, RID p_by_texture) = 0; virtual void texture_set_size_override(RID p_texture, int p_width, int p_height) = 0; virtual void texture_set_path(RID p_texture, const String &p_path) = 0; @@ -148,6 +149,9 @@ class RenderingServer : public Object { virtual void texture_set_detect_roughness_callback(RID p_texture, RenderingServerTypes::TextureDetectRoughnessCallback p_callback, void *p_userdata) = 0; virtual void texture_debug_usage(List *r_info) = 0; + + virtual void texture_2d_attach_streaming_state(RID p_texture, RID p_streaming_state) = 0; + Array _texture_debug_usage_bind(); virtual void texture_set_force_redraw_if_visible(RID p_texture, bool p_enable) = 0; diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index cc9518f86079..6f2d80a30c71 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -244,6 +244,7 @@ class RenderingServerDefault : public RenderingServer { FUNC0RC(RID, texture_drawable_get_default_material) FUNC2(texture_replace, RID, RID) + FUNC2(texture_replace_compatible, RID, RID) FUNC3(texture_set_size_override, RID, int, int) // FIXME: Disabled during Vulkan refactoring, should be ported. @@ -267,6 +268,8 @@ class RenderingServerDefault : public RenderingServer { FUNC2RC(RID, texture_get_rd_texture, RID, bool) FUNC2RC(uint64_t, texture_get_native_handle, RID, bool) + FUNC2(texture_2d_attach_streaming_state, RID, RID); + /* SHADER API */ #undef ServerName diff --git a/servers/rendering/shader_compiler.cpp b/servers/rendering/shader_compiler.cpp index 3f0e2a45c821..6ec1b8ace22b 100644 --- a/servers/rendering/shader_compiler.cpp +++ b/servers/rendering/shader_compiler.cpp @@ -475,6 +475,11 @@ String ShaderCompiler::_dump_node_code(const SL::Node *p_node, int p_level, Gene // Stencil modes. for (int i = 0; i < pnode->stencil_modes.size(); i++) { + if (p_default_actions.stencil_mode_defines.has(pnode->stencil_modes[i]) && !used_smode_defines.has(pnode->stencil_modes[i])) { + r_gen_code.defines.push_back(p_default_actions.stencil_mode_defines[pnode->stencil_modes[i]]); + used_smode_defines.insert(pnode->stencil_modes[i]); + } + if (p_actions.stencil_mode_values.has(pnode->stencil_modes[i])) { Pair &p = p_actions.stencil_mode_values[pnode->stencil_modes[i]]; *p.first = p.second; @@ -1497,6 +1502,15 @@ String ShaderCompiler::_dump_node_code(const SL::Node *p_node, int p_level, Gene code = "return;"; } } else if (cfnode->flow_op == SL::FLOW_OP_DISCARD) { + if (p_default_actions.usage_defines.has("DISCARD") && !used_name_defines.has("DISCARD")) { + String define = p_default_actions.usage_defines["DISCARD"]; + if (define.begins_with("@")) { + define = p_default_actions.usage_defines[define.substr(1)]; + } + r_gen_code.defines.push_back(define); + used_name_defines.insert("DISCARD"); + } + if (p_actions.usage_flag_pointers.has("DISCARD") && !used_flag_pointers.has("DISCARD")) { *p_actions.usage_flag_pointers["DISCARD"] = true; used_flag_pointers.insert("DISCARD"); @@ -1647,6 +1661,7 @@ Error ShaderCompiler::compile(RSE::ShaderMode p_mode, const String &p_code, Iden used_name_defines.clear(); used_rmode_defines.clear(); + used_smode_defines.clear(); used_flag_pointers.clear(); fragment_varyings.clear(); diff --git a/servers/rendering/shader_compiler.h b/servers/rendering/shader_compiler.h index 93afcd8eeee7..b1796a9f8c16 100644 --- a/servers/rendering/shader_compiler.h +++ b/servers/rendering/shader_compiler.h @@ -95,6 +95,7 @@ class ShaderCompiler { struct DefaultIdentifierActions { HashMap renames; HashMap render_mode_defines; + HashMap stencil_mode_defines; HashMap usage_defines; HashMap custom_samplers; ShaderLanguage::TextureFilter default_filter = ShaderLanguage::TextureFilter::FILTER_NEAREST; @@ -126,6 +127,7 @@ class ShaderCompiler { HashSet used_name_defines; HashSet used_flag_pointers; HashSet used_rmode_defines; + HashSet used_smode_defines; HashSet internal_functions; HashSet fragment_varyings; diff --git a/servers/rendering/shader_types.cpp b/servers/rendering/shader_types.cpp index dce5b1c934b3..c4954fbc6c80 100644 --- a/servers/rendering/shader_types.cpp +++ b/servers/rendering/shader_types.cpp @@ -146,6 +146,7 @@ ShaderTypes::ShaderTypes() { shader_modes[RSE::SHADER_SPATIAL].functions["fragment"].built_ins["NORMAL_MAP_DEPTH"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RSE::SHADER_SPATIAL].functions["fragment"].built_ins["BENT_NORMAL_MAP"] = ShaderLanguage::TYPE_VEC3; shader_modes[RSE::SHADER_SPATIAL].functions["fragment"].built_ins["UV"] = constt(ShaderLanguage::TYPE_VEC2); + shader_modes[RSE::SHADER_SPATIAL].functions["fragment"].built_ins["STREAMING_UV"] = ShaderLanguage::TYPE_VEC2; shader_modes[RSE::SHADER_SPATIAL].functions["fragment"].built_ins["UV2"] = constt(ShaderLanguage::TYPE_VEC2); shader_modes[RSE::SHADER_SPATIAL].functions["fragment"].built_ins["COLOR"] = constt(ShaderLanguage::TYPE_VEC4); shader_modes[RSE::SHADER_SPATIAL].functions["fragment"].built_ins["ALBEDO"] = ShaderLanguage::TYPE_VEC3; diff --git a/servers/rendering/storage/texture_storage.h b/servers/rendering/storage/texture_storage.h index 243d973c8385..5a0d7bc69e5f 100644 --- a/servers/rendering/storage/texture_storage.h +++ b/servers/rendering/storage/texture_storage.h @@ -94,6 +94,7 @@ class RendererTextureStorage { virtual RID texture_drawable_get_default_material() const = 0; virtual void texture_replace(RID p_texture, RID p_by_texture) = 0; + virtual void texture_replace_compatible(RID p_texture, RID p_by_texture) = 0; virtual void texture_set_size_override(RID p_texture, int p_width, int p_height) = 0; virtual void texture_set_path(RID p_texture, const String &p_path) = 0; @@ -211,4 +212,7 @@ class RendererTextureStorage { // Motion vectors virtual void render_target_set_velocity_target_size(RID p_render_target, const Size2i &p_target_size) = 0; virtual Size2i render_target_get_velocity_target_size(RID p_render_target) const = 0; + + // Texture Streaming + virtual void texture_2d_attach_streaming_state(RID p_texture, RID p_streaming_state) = 0; }; diff --git a/servers/server_wrap_mt_common.h b/servers/server_wrap_mt_common.h index c55b91b849fa..0ad1ca13b75b 100644 --- a/servers/server_wrap_mt_common.h +++ b/servers/server_wrap_mt_common.h @@ -436,7 +436,7 @@ } #define FUNC5R(m_r, m_type, m_arg1, m_arg2, m_arg3, m_arg4, m_arg5) \ - virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) { \ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) override { \ WRITE_ACTION \ if (ASYNC_COND_PUSH_AND_RET) { \ m_r ret; \