OCIO GPU / MSL: `TEXTURE_RGB_CHANNEL` Reported for Single-Channel 1D LUTs

未关闭
#2,272 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
停滞
技术栈
cpp

调研方向

首先使用 GPU_LANGUAGE_MSL_2_0 重现 GPU/CPU 差异,同时检查 GpuShaderDesc::getTexture()、生成的 MSL 以及 CPUProcessor::applyRGB。根据命名的 1D LUT 所使用的辅助函数返回类型,验证报告的通道数。元数据和缓冲区布局一致,并且有一个覆盖 ACES 2.0 变换的集成测试,即表示完成。

由索引模型根据 Issue 内容生成。

描述

I've been doing some Vibing with OCIO and Metal recently and ran into a bit of weirdness implementing the ACES 2.0 Output Transforms in a Metal App.

My mate Claude got it working, but wanted me to share this issue.

OCIO GPU / MSL: TEXTURE_RGB_CHANNEL Reported for Single-Channel 1D LUTs

Summary

When using OCIO's GPU shader path with GPU_LANGUAGE_MSL_2_0, the
GpuShaderDesc::getTexture() API reports TEXTURE_RGB_CHANNEL for some 1D
LUTs that are actually single-channel
. The generated MSL shader code
contradicts this — it declares those textures as texture1d<float> and only
ever reads the .r component via a float-returning helper function.
Allocating a buffer based on the reported channel count causes a 3× buffer
over-read
, uploading two extra pages of uninitialised heap memory as LUT data,
and producing completely wrong (or effectively black) rendered output.

Confirmed with:

  • OCIO 2.5.0
  • GPU_LANGUAGE_MSL_2_0
  • ACES studio config v4.0.0 / ACES v2.0 / OCIO v2.5
  • Display transform: ACES 2.0 (ACES Output Transform v2.0)
  • Platform: Apple Silicon / Metal

Background

The ACES 2.0 Output Transform uses two 1D LUTs generated at shader-compilation
time:

Texture name OCIO-reported channels Shader type Shader accessor
ocio_reach_m_table_0 TEXTURE_RGB_CHANNEL (3) texture1d<float> .r only
ocio_gamut_cusp_table_0 TEXTURE_RGB_CHANNEL (3) texture1d<float> .r only (per axis, via a loop)

Both textures are reported as 3-channel by the API. Both are only
single-channel in reality (and in the generated shader).


The Bug

When a Metal (or any GPU) application queries LUT metadata and allocates upload
buffers using the reported channel count, it does:

// Reported by GpuShaderDesc::getTexture():
//   width  = 362
//   height = 1
//   channel = TEXTURE_RGB_CHANNEL  →  channelCount = 3
size_t dataSize = width * height * channelCount * sizeof(float);
//             = 362   * 1      * 3             * 4
//             = 4344 bytes

// But OCIO only writes 362 * 1 * sizeof(float) = 1448 bytes into `values`
NSData *data = [NSData dataWithBytes:values length:dataSize];
//                                            ^^^^ reads 2896 extra bytes

The values pointer returned by getTexture() only contains
width × 1 × sizeof(float) = 1448 bytes of valid data. Reading 4344 bytes
past it yields garbage values from uninitialised heap memory. In our case,
LUT texels past index ~120 contained values such as 2.88e32, causing the
ACES 2.0 gamut-compression path to fail silently and produce (0, 0, 0) for
essentially every pixel.


Diagnosis

Step 1 — CPU / GPU comparison

Running the same transform on the CPU reference path
(OCIO::CPUProcessor::applyRGB) gave correct output immediately. The GPU path
produced near-black. This pointed to the LUT data, not the shader math.

Step 2 — Raw buffer inspection

Dumping the raw floats that were being uploaded to the reach_m_table_0
texture:

texel[119] = 394.51   ✅ valid
texel[120] = 393.89   ✅ valid
texel[121] = 2.88e32  ❌ garbage
texel[122] = 7.56e28  ❌ garbage

The corruption boundary at ~texel 120 corresponds exactly to the valid
1-channel length (1448 / 4 = 362 / 3 ≈ 120).

Step 3 — Shader inspection

The OCIO-generated MSL for both textures:

// Texture declaration — note: texture1d<float>, not texture1d<float3>
void ocio_reach_m_table_0_sample(float index,
                                  texture1d<float> lut,
                                  sampler samp,
                                  thread float & outValue)
{
    float fi = (index + 0.5) / 362.0;
    outValue = lut.sample(samp, fi).r;   // ← only .r
}

The return type is float, not float3. This is the definitive indicator that
only one channel of data is present, regardless of what getTexture() reports.


Root Cause

GpuShaderDesc::getTexture() returns TEXTURE_RGB_CHANNEL for these textures,
but:

  1. OCIO's internal buffer for reach_m_table_0 and gamut_cusp_table_0
    contains only width × 1 × sizeof(float) bytes of valid data.
  2. The generated MSL shader accesses only the .r channel.
  3. The channel enum value does not accurately describe the data layout for these
    particular LUTs.

It is unclear whether this is an intentional convention (the enum reflects the
GPU texture format that should be created, which for texture1d<float> has an
implicit R-only format), or a straightforward bug in how OCIO populates the enum
for scalar 1D LUTs. Either way, blindly allocating width * channelCount * 4
bytes and passing that to dataWithBytes:length: is unsafe.


Fix

The reliable discriminator is the return type of the OCIO-generated helper
function
:

  • float <textureName>_sample(...) → single-channel, allocate width * 1 * 4 bytes
  • float3 <textureName>_sample(...) → three-channel, allocate width * 3 * 4 bytes
/// Returns true if the generated shader treats this texture as multi-channel.
/// OCIO always emits `float3 <name>_sample(...)` for RGB textures and
/// `float <name>_sample(...)` for R-only textures.
private func shaderSamplesRGB(textureName: String, shaderCode: String) -> Bool {
    if shaderCode.contains("float3 \(textureName)_sample") { return true  }
    if shaderCode.contains("float \(textureName)_sample")  { return false }
    return false  // safe default: treat as single-channel
}

Applied during LUT buffer allocation:

var channels = textureInfo["channels"] as? Int ?? 1  // from getTexture()
if channels == 3 && !shaderSamplesRGB(textureName: name, shaderCode: metalShaderCode) {
    // OCIO reports 3ch but shader only reads .r — override to avoid over-read
    channels = 1
}
let validFloatCount = width * channels
// allocate / copy only `validFloatCount * sizeof(float)` bytes

This check is zero-cost (a single string scan of the already-retrieved shader
source) and correctly handles both cases:

Texture getTexture() reports Helper fn return type Effective channels
ocio_reach_m_table_0 TEXTURE_RGB_CHANNEL float 1 (corrected)
ocio_gamut_cusp_table_0 TEXTURE_RGB_CHANNEL float 1 (corrected)

Note: in theory a future OCIO build might produce a different transform
with a genuinely 3-channel float3-returning 1D LUT. The helper-function
approach handles that correctly too, because it reads the actual generated code
rather than relying on the metadata enum.


Recommendations for the OCIO Project

  1. Documentation: Clarify whether TEXTURE_RGB_CHANNEL on a 1D LUT means
    "the data buffer contains RGB interleaved floats" or "you should create an
    RGB-format GPU texture" (which for 1D textures is ambiguous).

  2. API alignment: If TEXTURE_RGB_CHANNEL is returned, the values pointer
    should point to width * 3 * sizeof(float) bytes of valid data, or the enum
    value should be TEXTURE_RED_CHANNEL when the data is scalar.

  3. Test coverage: Add a Metal/MSL integration test that round-trips the ACES
    2.0 Output Transform through the GPU path and compares output against
    CPUProcessor for at least one known pixel value.


Reproduction

  • Open any ACES 2065-1 scene-linear EXR in an application using OCIO's MSL GPU
    path with the ACES studio config v4.0.0.
  • Apply the display transform "Display P3 HDR - Display / ACES 2.0 - HDR 1000 nits".
  • Allocate LUT upload buffers using width * channelCount * sizeof(float) where
    channelCount is derived from getTexture()'s channels parameter.
  • Compare GPU output to CPU reference — GPU will be effectively black for all
    pixels that pass through the ACES 2.0 gamut-compression path (i.e. nearly
    every pixel in a typical scene).

主要语言
C++
星标
2.1k
派生
505
平均合并
12 小时 24 分钟
30 天内合并 PR
2

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

AcademySoftwareFoundation/OpenColorIO 的其他 Issue

查看 AcademySoftwareFoundation/OpenColorIO 的全部 Issue

相似的 Issue

更多 C++ Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。