Files
filament/libs/utils/src/FeatureFlagManager.cpp
Eliza 2f1266f7dd engine: add program cache (#9297)
* engine: add program cache

This is another chunky change.

The core of this change is to cache programs in MaterialCache according to a
"specialization" (ProgramSpecialization) which is defined as the program cache
ID (the same key used for the OpenGL binary blob cache), the variant, and the
set of spec constants.

As part of this change, a lot of the implementation details of shader
compilation were refactored from Material to MaterialDefinition. The resulting
flow is a lot cleaner and easier to reason about, since shader compilation is
now a pure function of the MaterialDefinition + ProgramSpecialization.

Since the global cache program lookups might take a bit of time to compute
hashes, etc, I left the set of cached programs in Material as well, which kind
of acts like an L1 cache. The effect is that prepareProgram() and getProgram()
should be no slower than HEAD, even with the more complex caching requirements.

I'm planning on writing a document about this (and all changes up until this
point), but I'm being asked to work on higher priority things and I wanted to
have this PR out for review in the meantime so it doesn't bitrot.

* engine: fix unit tests

* engine: fix spec constants intern pool memory leak

* engine: address program cache comments

* engine: address more program cache comments

* engine: matdbg support for program cache

* engine: reinstate descriptorLayout calls

* engine: address bitrot

* engine: add feature flag to disable program cache

* engine: use material CRC32 for program cache

The "cache ID" of a material is supposed to uniquely identify a shader program
and all its variants. This is true to a certain extent, but does not account for
the code generation that happens at runtime. Two materials may have "identical"
shader programs, but due to each material's differing unique metadata, the final
compiled programs may end up very different. Unfortunately, this means we cannot
rely on the "cache ID" alone to determine a shader program's reusability.

Ideally, we should hash this "cache ID" with the exact set of changes to each
shader program so that we could reuse programs across materials. Instead, as a
stopgap solution, use the material's CRC32 instead.

* engine: fix double-free in program cache

* engine: address comments

* engine: assert_invariant empty material cache
2026-01-27 13:09:42 -08:00

177 lines
7.8 KiB
C++

/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <private/utils/FeatureFlagManager.h>
#include <utils/compiler.h>
#include <utils/debug.h>
#include <utils/Logger.h>
#include <utils/Panic.h>
#include <utils/Slice.h>
#include <algorithm>
#include <array>
#include <optional>
#include <string_view>
#include <stdlib.h>
#include <stdio.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif
namespace utils {
namespace {
#ifdef __ANDROID__
std::string_view getFeatureFlagFromEnvironment(char const* const name, std::array<char, 128>& storage) {
// since API 26 there is no limit to the property name
char propertyName[128];
snprintf(propertyName, 128, "debug.%s", name);
int const length = __system_property_get(propertyName, storage.data());
if (length > 0) {
return { storage.data(), size_t(length) };
}
return {};
}
#else
std::string_view getFeatureFlagFromEnvironment(char const* const name, std::array<char, 128>&) {
char const* const env = getenv(name);
// ReSharper disable once CppDFALocalValueEscapesFunction
return env ? std::string_view{env} : std::string_view{};
}
#endif
void overrideFeatureDefaults(utils::Slice<FeatureFlagManager::FeatureFlag> const& features) {
std::array<char, 128> storage;
UTILS_NOUNROLL
for (auto const& feature : features) {
std::string_view const value = getFeatureFlagFromEnvironment(feature.name, storage);
if (!value.empty()) {
if (value == "1" || value == "true") {
*const_cast<bool*>(feature.value) = true;
} else if (value == "0" || value == "false") {
*const_cast<bool*>(feature.value) = false;
}
DLOG(INFO) << "overriding " << feature.name << " to " << *feature.value;
}
}
}
} // anonymous
FeatureFlagManager::FeatureFlagManager() : mFeatures{{
{ "backend.disable_parallel_shader_compile",
"Disable parallel shader compilation in GL and Metal backends.",
&features.backend.disable_parallel_shader_compile },
{ "backend.disable_amortized_shader_compile",
"Disable amortized shader compilation in GL backend.",
&features.backend.disable_amortized_shader_compile },
{ "backend.disable_handle_use_after_free_check",
"Disable Handle<> use-after-free checks.",
&features.backend.disable_handle_use_after_free_check },
{ "backend.disable_heap_handle_tags",
"Disable Handle<> tags for heap-allocated handles.",
&features.backend.disable_heap_handle_tags },
{ "backend.enable_asynchronous_operation",
"Enable asynchronous operation for resource management.",
&features.backend.enable_asynchronous_operation },
{ "backend.opengl.assert_native_window_is_valid",
"Asserts that the ANativeWindow is valid when rendering starts.",
&features.backend.opengl.assert_native_window_is_valid },
{ "engine.color_grading.use_1d_lut",
"Uses a 1D LUT for color grading.",
&features.engine.color_grading.use_1d_lut, false },
{ "engine.shadows.use_shadow_atlas",
"Uses an array of atlases to store shadow maps.",
&features.engine.shadows.use_shadow_atlas, false },
{ "engine.debug.assert_material_instance_in_use",
"Assert when a MaterialInstance is destroyed while it is in use by RenderableManager.",
&features.engine.debug.assert_material_instance_in_use, false },
{ "engine.debug.assert_destroy_material_before_material_instance",
"Assert when a Material is destroyed but its instances are still alive.",
&features.engine.debug.assert_destroy_material_before_material_instance, false },
{ "engine.debug.assert_vertex_buffer_count_exceeds_8",
"Assert when a client's number of buffers for a VertexBuffer exceeds 8.",
&features.engine.debug.assert_vertex_buffer_count_exceeds_8, false },
{ "engine.debug.assert_vertex_buffer_attribute_stride_mult_of_4",
"Assert that the attribute stride of a vertex buffer is a multiple of 4.",
&features.engine.debug.assert_vertex_buffer_attribute_stride_mult_of_4, false },
{ "backend.vulkan.enable_pipeline_cache_prewarming",
"Enables an experimental approach to parallel shader compilation on Vulkan.",
&features.backend.vulkan.enable_pipeline_cache_prewarming, false },
{ "engine.enable_program_cache",
"Enable the shader program cache.",
&features.engine.enable_program_cache },
{ "backend.vulkan.enable_staging_buffer_bypass",
"vulkan: enable a staging bypass logic for unified memory architecture.",
&features.backend.vulkan.enable_staging_buffer_bypass, false },
{ "backend.vulkan.enable_acquire_swapchain_in_make_current",
"vulkan: acquire the swapchain in makeCurrent() instead of beginRenderPass().",
&features.backend.vulkan.enable_acquire_swapchain_in_make_current, false },
{ "engine.debug.assert_material_instance_texture_descriptor_set_compatible",
"Assert that the textures in a material instance are compatible with descriptor set.",
&features.engine.debug.assert_material_instance_texture_descriptor_set_compatible, false },
{ "engine.debug.assert_texture_can_generate_mipmap",
"Assert if a texture has the correct usage set for generating mipmaps.",
&features.engine.debug.assert_texture_can_generate_mipmap, false },
{ "material.check_crc32_after_loading",
"Verify the checksum of package data when a material is loaded.",
&features.material.check_crc32_after_loading, false },
{ "material.enable_material_instance_uniform_batching",
"Make all MaterialInstances share a common large uniform buffer and use sub-allocations within it.",
&features.material.enable_material_instance_uniform_batching },
{ "engine.frame_info.disable_gpu_complete_metric",
"Disable Renderer::FrameInfo::gpuFrameComplete reporting",
&features.engine.frame_info.disable_gpu_frame_complete_metric },
{ "engine.skip_frame_when_cpu_ahead_of_display",
"Automatically skip frames when the CPU gets ahead of the display.",
&features.engine.skip_frame_when_cpu_ahead_of_display },
}} {
overrideFeatureDefaults({ mFeatures.data(), mFeatures.size() });
}
FeatureFlagManager::~FeatureFlagManager() noexcept = default;
bool FeatureFlagManager::setFeatureFlag(char const* name, bool const value) noexcept {
auto* const p = getFeatureFlagPtr(name);
if (p) {
*p = value;
}
return p != nullptr;
}
std::optional<bool> FeatureFlagManager::getFeatureFlag(char const* name) const noexcept {
if (auto* const p = getFeatureFlagPtr(name, true)) {
return *p;
}
return std::nullopt;
}
bool* FeatureFlagManager::getFeatureFlagPtr(std::string_view name, bool const allowConstant) const noexcept {
auto const pos = std::find_if(mFeatures.begin(), mFeatures.end(),
[name](FeatureFlag const& entry) {
return name == entry.name;
});
return (pos != mFeatures.end() && (!pos->constant || allowConstant)) ?
const_cast<bool*>(pos->value) : nullptr;
}
} // namespace filament