Skip to content

Commit 1e3a258

Browse files
ColorGrading: support Rec709-Linear-D65 in NEON LUT generation (#10351)
Enable ColorGrading objects configured with Rec709-Linear-D65 output color space to take advantage of optimized NEON fast-paths (generateMediumLUTNeon). Previously, the NEON builders were guarded by outputColorSpace == Rec709-sRGB-D65 due to the hardcoded v_oetf_sRGB step, forcing linear output configurations to fall back to the slower scalar generation path.
1 parent 39d4497 commit 1e3a258

2 files changed

Lines changed: 141 additions & 5 deletions

File tree

filament/src/details/ColorGrading.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,9 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
868868
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
869869
#endif
870870
bool const isSupportedType = type == PixelDataType::UINT_2_10_10_10_REV;
871+
// Medium state supports linear or sRGB.
872+
bool const isSupportedColorSpace = builder->outputColorSpace == Rec709-sRGB-D65 ||
873+
builder->outputColorSpace == Rec709-Linear-D65;
871874

872875
bool const isDefaultState = !mIsOneDimensional &&
873876
!builder->hasAdjustments &&
@@ -879,15 +882,15 @@ FColorGrading::FColorGrading(FEngine& engine, const Builder& builder) {
879882
engine.features.engine.color_grading.use_optimized_default_builder &&
880883
builder->toneMapping == ToneMapping::ACES_LEGACY &&
881884
builder->outputColorSpace == Rec709-sRGB-D65 &&
882-
type == PixelDataType::UINT_2_10_10_10_REV &&
885+
isSupportedType &&
883886
(config.lutDimension & (config.lutDimension - 1)) == 0 &&
884887
(config.lutDimension * config.lutDimension * config.lutDimension) % 4 == 0;
885888

886889
bool const isMediumState = !isDefaultState &&
887890
!mIsOneDimensional &&
888891
builder->fastMath &&
889892
engine.features.engine.color_grading.use_optimized_default_builder &&
890-
builder->outputColorSpace == Rec709-sRGB-D65 &&
893+
isSupportedColorSpace &&
891894
isSupportedType &&
892895
(config.lutDimension & (config.lutDimension - 1)) == 0 &&
893896
(config.lutDimension * config.lutDimension * config.lutDimension) % 4 == 0;
@@ -1097,7 +1100,6 @@ float4 FColorGrading::hdrColorAt(Builder const& builder, Config const& config,
10971100

10981101
#if defined(__ARM_NEON)
10991102

1100-
UTILS_NOINLINE
11011103
void FColorGrading::generateDefaultLUTNeon(FEngine const& engine, void* data,
11021104
Config const& config, Builder const& builder) noexcept {
11031105
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
@@ -1184,14 +1186,15 @@ UTILS_NOINLINE
11841186
void FColorGrading::generateMediumLUTNeon(FEngine const& engine, void* data, Config const& config, Builder const& builder) noexcept {
11851187
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
11861188

1189+
bool const isSrgb = (builder->outputColorSpace == Rec709-sRGB-D65);
11871190
uint32_t const dim = config.lutDimension;
11881191
assert_invariant((dim & (dim - 1)) == 0); // dim is power of 2
11891192

11901193
JobSystem& js = engine.getJobSystem();
11911194
auto *slices = js.createJob();
11921195

11931196
for (uint32_t b = 0; b < dim; b++) {
1194-
auto work = [data, b, &config, &builder](JobSystem&, JobSystem::Job*) {
1197+
auto work = [data, b, &config, &builder, &isSrgb](JobSystem&, JobSystem::Job*) {
11951198
FILAMENT_TRACING_NAME(FILAMENT_TRACING_CATEGORY_FILAMENT, "ColorGrading::jobNeon");
11961199
uint32_t const dim = config.lutDimension;
11971200
uint32_t const mask = dim - 1;
@@ -1303,7 +1306,9 @@ void FColorGrading::generateMediumLUTNeon(FEngine const& engine, void* data, Con
13031306
cg_g = vmaxq_f32(vminq_f32(cg_g, vdupq_n_f32(1.0f)), vdupq_n_f32(0.0f));
13041307
cg_b = vmaxq_f32(vminq_f32(cg_b, vdupq_n_f32(1.0f)), vdupq_n_f32(0.0f));
13051308

1306-
v_oetf_sRGB(cg_r, cg_g, cg_b);
1309+
if (UTILS_LIKELY(isSrgb)) {
1310+
v_oetf_sRGB(cg_r, cg_g, cg_b);
1311+
}
13071312

13081313
if (UTILS_UNLIKELY(!builder->customLutData.empty())) {
13091314
auto const* clData = builder->customLutData.data();

filament/test/filament_test.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <filament/Camera.h>
3333
#include <filament/Color.h>
3434
#include <filament/ColorGrading.h>
35+
#include <filament/ColorSpace.h>
3536
#include <filament/Engine.h>
3637
#include <filament/FrameHistoryStream.h>
3738
#include <filament/Frustum.h>
@@ -66,6 +67,7 @@
6667

6768
using namespace filament;
6869
using namespace filament::math;
70+
using namespace filament::color;
6971
using namespace utils;
7072

7173
static bool isGray(float3 const v) {
@@ -1269,6 +1271,135 @@ TEST(FilamentTest, ColorGradingMediumNeonValidation) {
12691271
Engine::destroy((Engine**)&engine);
12701272
}
12711273

1274+
1275+
TEST(FilamentTest, ColorGradingNeonLinearValidation) {
1276+
Engine* engine = Engine::Builder()
1277+
.backend(Engine::Backend::NOOP)
1278+
.feature("engine.color_grading.use_optimized_default_builder", true)
1279+
.build();
1280+
ASSERT_NE(engine, nullptr);
1281+
1282+
struct LutData {
1283+
std::vector<uint32_t> pixels;
1284+
uint32_t width, height, depth;
1285+
};
1286+
1287+
auto exporter = [](void const* data, size_t const size, backend::PixelDataFormat format,
1288+
backend::PixelDataType type, uint32_t const w, uint32_t const h, uint32_t const d, void* user) {
1289+
auto* target = static_cast<LutData*>(user);
1290+
target->width = w; target->height = h; target->depth = d;
1291+
target->pixels.resize(size / sizeof(uint32_t));
1292+
memcpy(target->pixels.data(), data, size);
1293+
};
1294+
1295+
LutData neonLut, scalarLut;
1296+
1297+
// 1. Generate Vectorized Linear LUT (fastMath = true)
1298+
ColorGrading const* cgNeon = ColorGrading::Builder()
1299+
.fastMath(true)
1300+
.outputColorSpace(Rec709-Linear-D65)
1301+
.exportLut(exporter, &neonLut)
1302+
.build(*engine);
1303+
1304+
// 2. Generate Scalar Linear LUT (fastMath = false)
1305+
ColorGrading const* cgScalar = ColorGrading::Builder()
1306+
.fastMath(false)
1307+
.outputColorSpace(Rec709-Linear-D65)
1308+
.exportLut(exporter, &scalarLut)
1309+
.build(*engine);
1310+
1311+
// 3. Verify Metadata and Pixels
1312+
ASSERT_EQ(neonLut.width, scalarLut.width);
1313+
ASSERT_EQ(neonLut.pixels.size(), scalarLut.pixels.size());
1314+
1315+
for (size_t i = 0; i < neonLut.pixels.size(); i++) {
1316+
uint32_t const cn = neonLut.pixels[i];
1317+
uint32_t const cs = scalarLut.pixels[i];
1318+
1319+
// Unpack 10-bit channels and verify with ±5 tolerance
1320+
EXPECT_NEAR(int(cn & 0x3FF), int(cs & 0x3FF), 5);
1321+
EXPECT_NEAR(int((cn >> 10) & 0x3FF), int((cs >> 10) & 0x3FF), 5);
1322+
EXPECT_NEAR(int((cn >> 20) & 0x3FF), int((cs >> 20) & 0x3FF), 5);
1323+
}
1324+
1325+
engine->destroy(cgNeon);
1326+
engine->destroy(cgScalar);
1327+
Engine::destroy((Engine**)&engine);
1328+
}
1329+
1330+
TEST(FilamentTest, ColorGradingMediumNeonLinearValidation) {
1331+
Engine* engine = Engine::Builder()
1332+
.backend(Engine::Backend::NOOP)
1333+
.feature("engine.color_grading.use_optimized_default_builder", true)
1334+
.build();
1335+
ASSERT_NE(engine, nullptr);
1336+
1337+
struct LutData {
1338+
std::vector<uint32_t> pixels;
1339+
uint32_t width, height, depth;
1340+
};
1341+
1342+
auto exporter = [](void const* data, size_t const size, backend::PixelDataFormat format,
1343+
backend::PixelDataType type, uint32_t const w, uint32_t const h, uint32_t const d, void* user) {
1344+
auto* target = static_cast<LutData*>(user);
1345+
target->width = w; target->height = h; target->depth = d;
1346+
target->pixels.resize(size / sizeof(uint32_t));
1347+
memcpy(target->pixels.data(), data, size);
1348+
};
1349+
1350+
LutData neonLut, scalarLut;
1351+
1352+
// 1. Generate Vectorized Medium Linear LUT (fastMath = true, hasAdjustments = true)
1353+
ColorGrading const* cgNeon = ColorGrading::Builder()
1354+
.fastMath(true)
1355+
.outputColorSpace(Rec709-Linear-D65)
1356+
.contrast(1.2f)
1357+
.saturation(1.1f)
1358+
.vibrance(1.15f)
1359+
.exposure(0.1f)
1360+
.shadowsMidtonesHighlights(
1361+
{0.95f, 1.0f, 1.05f, 0.0f},
1362+
{1.0f, 1.05f, 1.0f, 0.0f},
1363+
{1.05f, 1.0f, 0.95f, 0.0f},
1364+
{0.0f, 0.33f, 0.55f, 1.0f})
1365+
.exportLut(exporter, &neonLut)
1366+
.build(*engine);
1367+
1368+
// 2. Generate Scalar Medium Linear LUT (fastMath = false, hasAdjustments = true)
1369+
ColorGrading const* cgScalar = ColorGrading::Builder()
1370+
.fastMath(false)
1371+
.outputColorSpace(Rec709-Linear-D65)
1372+
.contrast(1.2f)
1373+
.saturation(1.1f)
1374+
.vibrance(1.15f)
1375+
.exposure(0.1f)
1376+
.shadowsMidtonesHighlights(
1377+
{0.95f, 1.0f, 1.05f, 0.0f},
1378+
{1.0f, 1.05f, 1.0f, 0.0f},
1379+
{1.05f, 1.0f, 0.95f, 0.0f},
1380+
{0.0f, 0.33f, 0.55f, 1.0f})
1381+
.exportLut(exporter, &scalarLut)
1382+
.build(*engine);
1383+
1384+
// 3. Verify Metadata and Pixels
1385+
ASSERT_EQ(neonLut.width, scalarLut.width);
1386+
ASSERT_EQ(neonLut.pixels.size(), scalarLut.pixels.size());
1387+
1388+
for (size_t i = 0; i < neonLut.pixels.size(); i++) {
1389+
uint32_t const cn = neonLut.pixels[i];
1390+
uint32_t const cs = scalarLut.pixels[i];
1391+
1392+
// Unpack 10-bit channels and verify with ±125 tolerance for chained Remez vs exact math
1393+
EXPECT_NEAR(int(cn & 0x3FF), int(cs & 0x3FF), 125);
1394+
EXPECT_NEAR(int((cn >> 10) & 0x3FF), int((cs >> 10) & 0x3FF), 125);
1395+
EXPECT_NEAR(int((cn >> 20) & 0x3FF), int((cs >> 20) & 0x3FF), 125);
1396+
}
1397+
1398+
engine->destroy(cgNeon);
1399+
engine->destroy(cgScalar);
1400+
Engine::destroy((Engine**)&engine);
1401+
}
1402+
12721403
TEST(FilamentTest, ColorGradingAdvancedNeonValidation) {
12731404
Engine* engine = Engine::Builder()
12741405
.backend(Engine::Backend::NOOP)

0 commit comments

Comments
 (0)