Loading...
Searching...
No Matches
YUVPlanar.hpp
1#pragma once
2#include <Gfx/Graph/encoders/GPUVideoEncoder.hpp>
3
4#include <memory>
5
6namespace score::gfx
7{
8
43{
44 // %1 = colorMatrixOut() defining convert_from_rgb(vec3)
45 // %2 = component selector (x/y/z)
46 // %3 = output scale (1.0 for R8, 1023.0/65535.0 for R16)
47 static constexpr const char* plane_frag = R"_(#version 450
48 layout(location = 0) in vec2 v_texcoord;
49 layout(location = 0) out vec4 fragColor;
50 layout(binding = 3) uniform sampler2D src_tex;
51 )_" "%1" R"_(
52 vec2 flip_y(vec2 tc) {
53 // Only OpenGL. The rest of the engine puts its geometry through
54 // renderer.clipSpaceCorrMatrix, which negates Y on Vulkan; this pass draws
55 // a hardcoded triangle in raw NDC and does not, so the correction it needs
56 // is not the same one. Flipping on Vulkan as well handed libav, GStreamer,
57 // NDI and every other consumer an upside-down picture.
58 #if defined(QSHADER_SPIRV) || defined(QSHADER_MSL) || defined(QSHADER_HLSL)
59 return tc;
60 #else
61 return vec2(tc.x, 1.0 - tc.y);
62 #endif
63 }
64 void main() {
65 vec3 rgb = texture(src_tex, flip_y(v_texcoord)).rgb;
66 vec3 yuv = clamp(convert_from_rgb(rgb), 0.0, 1.0);
67 fragColor = vec4(yuv.%2 * float(%3), 0.0, 0.0, 1.0);
68 }
69 )_";
70
71#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
72 // Qt < 6.10 10-bit fallback: pack 16-bit LE samples into RGBA8 targets.
73 // Helpers shared by the packed plane shaders (injected as %2).
74 static constexpr const char* pack_common = R"_(
75 int flip_y_int(int y, int h) {
76 // See GPUVideoEncoder::y_flip_glsl: only OpenGL. The rest of the engine
77 // negates Y through renderer.clipSpaceCorrMatrix on Vulkan; this pass
78 // indexes texels directly and does not, so it must not flip there.
79 #if defined(QSHADER_SPIRV) || defined(QSHADER_MSL) || defined(QSHADER_HLSL)
80 return y;
81 #else
82 return h - 1 - y;
83 #endif
84 }
85 uint comp10(vec3 rgb, int c) {
86 vec3 yuv = clamp(convert_from_rgb(rgb), 0.0, 1.0);
87 float v = (c == 0) ? yuv.x : (c == 1 ? yuv.y : yuv.z);
88 return uint(v * 1023.0 + 0.5);
89 }
90 // Two 16-bit values -> 4 little-endian bytes in an RGBA8 texel.
91 vec4 pack2(uint a, uint b) {
92 return vec4(float(a & 0xFFu), float((a >> 8u) & 0xFFu),
93 float(b & 0xFFu), float((b >> 8u) & 0xFFu)) / 255.0;
94 }
95 )_";
96
97 // %1 = colorMatrixOut(), %2 = pack_common. Y plane: 2 full-res luma/texel.
98 static constexpr const char* pack_y_frag = R"_(#version 450
99 layout(location = 0) in vec2 v_texcoord;
100 layout(location = 0) out vec4 fragColor;
101 layout(binding = 3) uniform sampler2D src_tex;
102 )_" "%1" R"_(
103 )_" "%2" R"_(
104 void main() {
105 ivec2 sz = textureSize(src_tex, 0);
106 ivec2 o = ivec2(gl_FragCoord.xy);
107 int sy = flip_y_int(o.y, sz.y);
108 int x0 = o.x * 2;
109 uint a = comp10(texelFetch(src_tex, ivec2(min(x0, sz.x - 1), sy), 0).rgb, 0);
110 uint b = comp10(texelFetch(src_tex, ivec2(min(x0 + 1, sz.x - 1), sy), 0).rgb, 0);
111 fragColor = pack2(a, b);
112 }
113 )_";
114
115 // %1 = colorMatrixOut(), %2 = pack_common, %3 = component (1=Cb, 2=Cr),
116 // %4 = chromaShiftY (0: 4:2:2, 1: 4:2:0). Each chroma sample averages the
117 // horizontal pair, and additionally the vertical pair when VS == 1 (for
118 // VS == 0 the y0/y1 rows coincide, so the 4-tap sum / 4 is the 2-tap mean).
119 static constexpr const char* pack_c_frag = R"_(#version 450
120 layout(location = 0) in vec2 v_texcoord;
121 layout(location = 0) out vec4 fragColor;
122 layout(binding = 3) uniform sampler2D src_tex;
123 )_" "%1" R"_(
124 )_" "%2" R"_(
125 uint chroma(int x, int oy, ivec2 sz) {
126 const int VS = %4;
127 int xa = min(x, sz.x - 1);
128 int xb = min(x + 1, sz.x - 1);
129 int y0 = flip_y_int(min((oy << VS), sz.y - 1), sz.y);
130 int y1 = flip_y_int(min((oy << VS) + VS, sz.y - 1), sz.y);
131 uint s = comp10(texelFetch(src_tex, ivec2(xa, y0), 0).rgb, %3)
132 + comp10(texelFetch(src_tex, ivec2(xb, y0), 0).rgb, %3)
133 + comp10(texelFetch(src_tex, ivec2(xa, y1), 0).rgb, %3)
134 + comp10(texelFetch(src_tex, ivec2(xb, y1), 0).rgb, %3);
135 return s >> 2u;
136 }
137 void main() {
138 ivec2 sz = textureSize(src_tex, 0);
139 ivec2 o = ivec2(gl_FragCoord.xy);
140 int x0 = o.x * 4;
141 fragColor = pack2(chroma(x0, o.y, sz), chroma(x0 + 2, o.y, sz));
142 }
143 )_";
144#endif
145
146 struct PlaneResources
147 {
148 QRhiTexture* texture{};
149 QRhiTextureRenderTarget* rt{};
150 QRhiRenderPassDescriptor* rp{};
151 QRhiShaderResourceBindings* srb{};
152 QRhiGraphicsPipeline* pipeline{};
153 QRhiReadbackResult readback{};
154 int w{}, h{};
155
156 void destroy()
157 {
158 delete pipeline; delete srb; delete rp; delete rt; delete texture;
159 pipeline = nullptr; srb = nullptr; rp = nullptr; rt = nullptr;
160 texture = nullptr;
161 }
162 };
163
164 int m_bits{8};
165 int m_chromaShiftY{0};
166 PlaneResources m_planes[3]; // Y, Cb, Cr
167 QRhiSampler* m_sampler{};
168 int m_width{};
169 int m_height{};
170
171 YUVPlanarEncoder(int bits, int chromaShiftY)
172 : m_bits{bits}
173 , m_chromaShiftY{chromaShiftY}
174 {
175 }
176
177 void initPlane(
178 QRhi& rhi, const RenderState& state, QRhiTexture* inputRGBA,
179 PlaneResources& plane, QRhiTexture::Format fmt, int w, int h,
180 const QString& frag)
181 {
182 plane.w = w;
183 plane.h = h;
184 plane.texture = rhi.newTexture(
185 fmt, QSize{w, h}, 1,
186 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
187 plane.texture->create();
188
189 plane.rt = rhi.newTextureRenderTarget({plane.texture});
190 plane.rp = plane.rt->newCompatibleRenderPassDescriptor();
191 plane.rt->setRenderPassDescriptor(plane.rp);
192 plane.rt->create();
193
194 plane.srb = rhi.newShaderResourceBindings();
195 plane.srb->setBindings({
196 QRhiShaderResourceBinding::sampledTexture(
197 3, QRhiShaderResourceBinding::FragmentStage, inputRGBA, m_sampler),
198 });
199 plane.srb->create();
200
201 auto [vs, fs] = makeShaders(state, QString::fromLatin1(vertex_shader), frag);
202 plane.pipeline = rhi.newGraphicsPipeline();
203 plane.pipeline->setShaderStages({
204 {QRhiShaderStage::Vertex, vs},
205 {QRhiShaderStage::Fragment, fs},
206 });
207 plane.pipeline->setVertexInputLayout({});
208 plane.pipeline->setShaderResourceBindings(plane.srb);
209 plane.pipeline->setRenderPassDescriptor(plane.rp);
210 plane.pipeline->create();
211 }
212
213 void execPlane(QRhi& rhi, QRhiCommandBuffer& cb, PlaneResources& p)
214 {
215 cb.beginPass(p.rt, Qt::black, {0.0f, 0});
216 cb.setGraphicsPipeline(p.pipeline);
217 cb.setShaderResources(p.srb);
218 cb.setViewport(QRhiViewport(0, 0, p.w, p.h));
219 cb.draw(3);
220 auto* batch = rhi.nextResourceUpdateBatch();
221 batch->readBackTexture(QRhiReadbackDescription{p.texture}, &p.readback);
222 cb.endPass(batch);
223 }
224
225 void init(
226 QRhi& rhi, const RenderState& state, QRhiTexture* inputRGBA, int width,
227 int height, const QString& colorConversion) override
228 {
229 m_width = width;
230 m_height = height;
231
232 m_sampler = rhi.newSampler(
233 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
234 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
235 m_sampler->create();
236
237 const int cw = width / 2;
238 const int ch = height >> m_chromaShiftY;
239
240#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
241 if(m_bits > 8)
242 {
243 // Qt < 6.10's GL backend reads R16 targets back as 8-bit RGBA; pack the
244 // 16-bit little-endian bytes into RGBA8 ourselves (two samples/texel)
245 // so the readback layout matches the native R16 path. Needs w % 4 == 0.
246 const QString cm = QString::fromLatin1(pack_common);
247 const QString vshift = QString::number(m_chromaShiftY);
248 const QString y = QString::fromLatin1(pack_y_frag).arg(colorConversion, cm);
249 const QString cb = QString::fromLatin1(pack_c_frag)
250 .arg(colorConversion, cm, QStringLiteral("1"), vshift);
251 const QString cr = QString::fromLatin1(pack_c_frag)
252 .arg(colorConversion, cm, QStringLiteral("2"), vshift);
253 initPlane(
254 rhi, state, inputRGBA, m_planes[0], QRhiTexture::RGBA8, width / 2, height,
255 y);
256 initPlane(
257 rhi, state, inputRGBA, m_planes[1], QRhiTexture::RGBA8, width / 4, ch, cb);
258 initPlane(
259 rhi, state, inputRGBA, m_planes[2], QRhiTexture::RGBA8, width / 4, ch, cr);
260 return;
261 }
262#endif
263
264 const QString scale = m_bits == 8 ? QStringLiteral("1.0")
265 : QStringLiteral("0.0156097"); // 1023/65535
266 auto frag = [&](const char* component) {
267 return QString::fromLatin1(plane_frag)
268 .arg(colorConversion)
269 .arg(component)
270 .arg(scale);
271 };
272 const auto fmt = m_bits == 8 ? QRhiTexture::R8 : QRhiTexture::R16;
273 initPlane(rhi, state, inputRGBA, m_planes[0], fmt, width, height, frag("x"));
274 initPlane(rhi, state, inputRGBA, m_planes[1], fmt, cw, ch, frag("y"));
275 initPlane(rhi, state, inputRGBA, m_planes[2], fmt, cw, ch, frag("z"));
276 }
277
278 void exec(QRhi& rhi, QRhiCommandBuffer& cb) override
279 {
280 execPlane(rhi, cb, m_planes[0]);
281 execPlane(rhi, cb, m_planes[1]);
282 execPlane(rhi, cb, m_planes[2]);
283 }
285 int planeCount() const override { return 3; }
286
287 const QRhiReadbackResult& readback(int plane) const override
288 {
289 return m_planes[plane].readback;
290 }
291
292 void release() override
293 {
294 for(auto& p : m_planes)
295 p.destroy();
296 delete m_sampler;
297 m_sampler = nullptr;
298 }
299
300 // NTV2_FBF_8BIT_YCBCR_422PL3
301 static std::unique_ptr<YUVPlanarEncoder> p422_8()
302 {
303 return std::make_unique<YUVPlanarEncoder>(8, 0);
304 }
305 // NTV2_FBF_8BIT_YCBCR_420PL3
306 static std::unique_ptr<YUVPlanarEncoder> p420_8()
307 {
308 return std::make_unique<YUVPlanarEncoder>(8, 1);
309 }
310 // NTV2_FBF_10BIT_YCBCR_420PL3_LE
311 static std::unique_ptr<YUVPlanarEncoder> p420_10()
312 {
313 return std::make_unique<YUVPlanarEncoder>(10, 1);
314 }
315};
316
317} // namespace score::gfx
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
std::pair< QShader, QShader > makeShaders(const RenderState &v, QString vert, QString frag, int multiViewCount)
Get a pair of compiled vertex / fragment shaders from GLSL 4.5 sources.
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:1238
Base class for GPU-side video format conversion (RGBA to YUV).
Definition GPUVideoEncoder.hpp:30
static constexpr const char * vertex_shader
Definition GPUVideoEncoder.hpp:74
Global state associated to a rendering context.
Definition RenderState.hpp:37
Generic 3-plane planar YCbCr encoder (8 or 10 bit, 4:2:2 or 4:2:0).
Definition YUVPlanar.hpp:43
void init(QRhi &rhi, const RenderState &state, QRhiTexture *inputRGBA, int width, int height, const QString &colorConversion) override
Definition YUVPlanar.hpp:222
const QRhiReadbackResult & readback(int plane) const override
Get the readback result for a given plane. Valid after endOffscreenFrame.
Definition YUVPlanar.hpp:284
void exec(QRhi &rhi, QRhiCommandBuffer &cb) override
Definition YUVPlanar.hpp:275
int planeCount() const override
Number of readback planes (1 for UYVY, 2 for NV12, 3 for I420).
Definition YUVPlanar.hpp:282
void release() override
Release all GPU resources.
Definition YUVPlanar.hpp:289