#version 450 // Copyright (c) 2025 Hans-Kristian Arntzen // SPDX-License-Identifier: MIT #extension GL_EXT_control_flow_attributes : require layout(location = 0) in vec2 vUV; layout(location = 0, component = 2) in float vIntCoord; #if CHROMA_CONFIG == 0 #define OUTPUT_PLANES 1 #define INPUT_PLANES 1 #elif CHROMA_CONFIG == 1 #define OUTPUT_PLANES 2 #define INPUT_PLANES 3 #elif CHROMA_CONFIG == 2 #define OUTPUT_PLANES 3 #define INPUT_PLANES 2 #else #error "Invalid chroma config" #endif layout(location = 0) out mediump float oY; #if OUTPUT_PLANES == 2 layout(location = 1) out mediump vec2 oCbCr; #elif OUTPUT_PLANES == 3 layout(location = 1) out mediump float oCb; layout(location = 2) out mediump float oCr; #endif layout(set = 0, binding = 0) uniform mediump texture2D uYEven; layout(set = 0, binding = 1) uniform mediump texture2D uYOdd; layout(set = 0, binding = 2) uniform mediump sampler uSampler; #if INPUT_PLANES == 3 layout(set = 0, binding = 3) uniform mediump texture2D uCbEven; layout(set = 0, binding = 4) uniform mediump texture2D uCbOdd; layout(set = 0, binding = 5) uniform mediump texture2D uCrEven; layout(set = 0, binding = 6) uniform mediump texture2D uCrOdd; #elif INPUT_PLANES == 2 layout(set = 0, binding = 3) uniform mediump texture2D uCbCrEven; layout(set = 0, binding = 4) uniform mediump texture2D uCbCrOdd; #endif // Direct and naive implementing of the CDF 9/7 synthesis filters. // Optimized for the mobile GPUs which don't have any // competent compute/shared memory performance whatsoever, // i.e. anything not AMD/NV/Intel. layout(constant_id = 0) const bool VERTICAL = false; layout(constant_id = 1) const bool FINAL_Y = false; layout(constant_id = 2) const bool FINAL_CBCR = false; layout(constant_id = 3) const int EDGE_CONDITION = 0; const ivec2 OFFSET_M2 = VERTICAL ? ivec2(0, 0) : ivec2(0, 0); const ivec2 OFFSET_M1 = VERTICAL ? ivec2(0, 1) : ivec2(1, 0); const ivec2 OFFSET_C = VERTICAL ? ivec2(0, 2) : ivec2(2, 0); const ivec2 OFFSET_P1 = VERTICAL ? ivec2(0, 3) : ivec2(3, 0); const ivec2 OFFSET_P2 = VERTICAL ? ivec2(0, 4) : ivec2(4, 0); const float SYNTHESIS_LP_0 = 1.11508705; const float SYNTHESIS_LP_1 = 0.591271763114; const float SYNTHESIS_LP_2 = -0.057543526229; const float SYNTHESIS_LP_3 = -0.091271763114; const float SYNTHESIS_HP_0 = 0.602949018236; const float SYNTHESIS_HP_1 = -0.266864118443; const float SYNTHESIS_HP_2 = -0.078223266529; const float SYNTHESIS_HP_3 = 0.016864118443; const float SYNTHESIS_HP_4 = 0.026748757411; layout(push_constant) uniform Registers { vec2 uv_offset; vec2 half_texel_offset; float res_scale; int aligned_transform_size; }; float[10] sample_component_gather(mediump texture2D tex_even, mediump texture2D tex_odd) { float components[10]; vec2 gather_uv = vUV + half_texel_offset; vec2 even0, even1, odd0, odd1; if (VERTICAL) { even0 = textureGatherOffset(sampler2D(tex_even, uSampler), gather_uv, OFFSET_M1).wx; even1 = textureGatherOffset(sampler2D(tex_even, uSampler), gather_uv, OFFSET_P1).wx; odd0 = textureGatherOffset(sampler2D(tex_odd, uSampler), gather_uv, OFFSET_M2).wx; odd1 = textureGatherOffset(sampler2D(tex_odd, uSampler), gather_uv, OFFSET_C).wx; } else { even0 = textureGatherOffset(sampler2D(tex_even, uSampler), gather_uv, OFFSET_M1).wz; even1 = textureGatherOffset(sampler2D(tex_even, uSampler), gather_uv, OFFSET_P1).wz; odd0 = textureGatherOffset(sampler2D(tex_odd, uSampler), gather_uv, OFFSET_M2).wz; odd1 = textureGatherOffset(sampler2D(tex_odd, uSampler), gather_uv, OFFSET_C).wz; } components[0] = 0.0; components[1] = odd0.x; components[2] = even0.x; components[3] = odd0.y; components[4] = even0.y; components[5] = odd1.x; components[6] = even1.x; components[7] = odd1.y; components[8] = even1.y; components[9] = textureLodOffset(sampler2D(tex_odd, uSampler), vUV, 0.0, OFFSET_P2).x; return components; } vec2[10] sample_component_gather2(mediump texture2D tex_even, mediump texture2D tex_odd) { vec2 components[10]; // Little point in using gather here, at least for now. components[0] = vec2(0.0); components[1] = textureLodOffset(sampler2D(tex_odd, uSampler), vUV, 0.0, OFFSET_M2).xy; components[2] = textureLodOffset(sampler2D(tex_even, uSampler), vUV, 0.0, OFFSET_M1).xy; components[3] = textureLodOffset(sampler2D(tex_odd, uSampler), vUV, 0.0, OFFSET_M1).xy; components[4] = textureLodOffset(sampler2D(tex_even, uSampler), vUV, 0.0, OFFSET_C).xy; components[5] = textureLodOffset(sampler2D(tex_odd, uSampler), vUV, 0.0, OFFSET_C).xy; components[6] = textureLodOffset(sampler2D(tex_even, uSampler), vUV, 0.0, OFFSET_P1).xy; components[7] = textureLodOffset(sampler2D(tex_odd, uSampler), vUV, 0.0, OFFSET_P1).xy; components[8] = textureLodOffset(sampler2D(tex_even, uSampler), vUV, 0.0, OFFSET_P2).xy; components[9] = textureLodOffset(sampler2D(tex_odd, uSampler), vUV, 0.0, OFFSET_P2).xy; return components; } void main() { bool is_odd = (int(vIntCoord) & 1) != 0; float Y[10] = sample_component_gather(uYEven, uYOdd); #if INPUT_PLANES == 2 vec2 CbCr[10] = sample_component_gather2(uCbCrEven, uCbCrOdd); #elif INPUT_PLANES == 3 float Cb[10] = sample_component_gather(uCbEven, uCbOdd); float Cr[10] = sample_component_gather(uCrEven, uCrOdd); vec2 CbCr[10]; [[unroll]] for (int i = 0; i < 10; i++) CbCr[i] = vec2(Cb[i], Cr[i]); #endif if (EDGE_CONDITION < 0) { // The mirroring rules are particular. // For odd inputs we can rely on the mirrored sampling to get intended behavior. if (vIntCoord < 1.0) { // Y4 is the pivot. Y[2] = Y[6]; #if INPUT_SAMPLES > 1 CbCr[2] = CbCr[6]; #endif } } else if (EDGE_CONDITION > 0) { if (vIntCoord + 2.0 > aligned_transform_size) { // We're on the last two pixels. // Y5 is the pivot. LP inputs behave as expected when using mirroring. Y[7] = Y[3]; Y[9] = Y[1]; #if INPUT_SAMPLES > 1 CbCr[7] = CbCr[3]; CbCr[9] = CbCr[1]; #endif } else if (vIntCoord + 4.0 >= aligned_transform_size) { // Y7 is the pivot. Y[9] = Y[5]; #if INPUT_SAMPLES > 1 CbCr[9] = CbCr[5]; #endif } } #if INPUT_PLANES > 1 #define AccumT vec3 #define GenInput(comp) vec3(Y[comp], CbCr[comp]) #else #define AccumT float #define GenInput(comp) Y[comp] #endif AccumT C0, C1, C2, C3, C4; float W0, W1, W2, W3, W4; // Not ideal, but gotta do what we gotta do. // GPU will have to take both paths here, // but at least we avoid dynamic load-store which is RIP perf on these chips ... if (is_odd) { C0 = GenInput(5); C1 = GenInput(4) + GenInput(6); C2 = GenInput(3) + GenInput(7); C3 = GenInput(2) + GenInput(8); C4 = GenInput(1) + GenInput(9); W0 = SYNTHESIS_HP_0; W1 = SYNTHESIS_LP_1; W2 = SYNTHESIS_HP_2; W3 = SYNTHESIS_LP_3; W4 = SYNTHESIS_HP_4; } else { C0 = GenInput(4); C1 = GenInput(3) + GenInput(5); C2 = GenInput(2) + GenInput(6); C3 = GenInput(1) + GenInput(7); C4 = AccumT(0.0); W0 = SYNTHESIS_LP_0; W1 = SYNTHESIS_HP_1; W2 = SYNTHESIS_LP_2; W3 = SYNTHESIS_HP_3; W4 = 0.0; } AccumT result = C0 * W0 + C1 * W1 + C2 * W2 + C3 * W3 + C4 * W4; #if OUTPUT_PLANES == 3 oY = result.x; oCb = result.y; oCr = result.z; #elif OUTPUT_PLANES == 2 oY = result.x; oCbCr = result.yz; #else oY = result; #endif if (FINAL_Y) oY += 0.5; if (FINAL_CBCR) { #if OUTPUT_PLANES == 3 oCb += 0.5; oCr += 0.5; #elif OUTPUT_PLANES == 2 oCbCr += 0.5; #endif } }