Loading...
Searching...
No Matches
encoders/NV12.hpp
1#pragma once
2#include <Gfx/Graph/encoders/GPUVideoEncoder.hpp>
3
4namespace score::gfx
5{
6
35{
36 // %1 = colorMatrixOut() shader defining convert_from_rgb(vec3)
37 static constexpr const char* y_frag = R"_(#version 450
38 layout(location = 0) in vec2 v_texcoord;
39 layout(location = 0) out vec4 fragColor;
40 layout(binding = 3) uniform sampler2D src_tex;
41 )_" "%1" R"_(
42 vec2 flip_y(vec2 tc) {
43 // Only OpenGL. The rest of the engine puts its geometry through
44 // renderer.clipSpaceCorrMatrix, which negates Y on Vulkan; this pass draws
45 // a hardcoded triangle in raw NDC and does not, so the correction it needs
46 // is not the same one. Flipping on Vulkan as well handed libav, GStreamer,
47 // NDI and every other consumer an upside-down picture.
48 #if defined(QSHADER_SPIRV) || defined(QSHADER_MSL) || defined(QSHADER_HLSL)
49 return tc;
50 #else
51 return vec2(tc.x, 1.0 - tc.y);
52 #endif
53 }
54 void main() {
55 vec3 rgb = texture(src_tex, flip_y(v_texcoord)).rgb;
56 vec3 yuv = convert_from_rgb(rgb);
57 fragColor = vec4(yuv.x, 0.0, 0.0, 1.0);
58 }
59 )_";
60
61 // Rendered at half resolution. Bilinear sampling averages the 2x2 block.
62 static constexpr const char* uv_frag = R"_(#version 450
63 layout(location = 0) in vec2 v_texcoord;
64 layout(location = 0) out vec4 fragColor;
65 layout(binding = 3) uniform sampler2D src_tex;
66 )_" "%1" R"_(
67 vec2 flip_y(vec2 tc) {
68 // Only OpenGL. The rest of the engine puts its geometry through
69 // renderer.clipSpaceCorrMatrix, which negates Y on Vulkan; this pass draws
70 // a hardcoded triangle in raw NDC and does not, so the correction it needs
71 // is not the same one. Flipping on Vulkan as well handed libav, GStreamer,
72 // NDI and every other consumer an upside-down picture.
73 #if defined(QSHADER_SPIRV) || defined(QSHADER_MSL) || defined(QSHADER_HLSL)
74 return tc;
75 #else
76 return vec2(tc.x, 1.0 - tc.y);
77 #endif
78 }
79 void main() {
80 vec3 rgb = texture(src_tex, flip_y(v_texcoord)).rgb;
81 vec3 yuv = convert_from_rgb(rgb);
82 fragColor = vec4(yuv.y, yuv.z, 0.0, 1.0);
83 }
84 )_";
85
86#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0)
87 // Qt < 6.10 fallback: interleaved UV bytes in an R8 target (width ×
88 // height/2). Output texel x holds U (x even) or V (x odd) of chroma site
89 // x/2, averaged over the 2x2 source block like the bilinear RG8 path.
90 static constexpr const char* uv_frag_packed = R"_(#version 450
91 layout(location = 0) in vec2 v_texcoord;
92 layout(location = 0) out vec4 fragColor;
93 layout(binding = 3) uniform sampler2D src_tex;
94 )_" "%1" R"_(
95 int flip_y_int(int y, int h) {
96 // See GPUVideoEncoder::y_flip_glsl: only OpenGL. The rest of the engine
97 // negates Y through renderer.clipSpaceCorrMatrix on Vulkan; this pass
98 // indexes texels directly and does not, so it must not flip there.
99 #if defined(QSHADER_SPIRV) || defined(QSHADER_MSL) || defined(QSHADER_HLSL)
100 return y;
101 #else
102 return h - 1 - y;
103 #endif
104 }
105 float chroma(ivec2 p, int c) {
106 vec3 yuv = clamp(convert_from_rgb(texelFetch(src_tex, p, 0).rgb), 0.0, 1.0);
107 return c == 1 ? yuv.y : yuv.z;
108 }
109 void main() {
110 ivec2 sz = textureSize(src_tex, 0);
111 ivec2 o = ivec2(gl_FragCoord.xy);
112 int c = ((o.x & 1) == 0) ? 1 : 2; // even byte: U, odd byte: V
113 int x0 = (o.x >> 1) * 2;
114 int xa = min(x0, sz.x - 1);
115 int xb = min(x0 + 1, sz.x - 1);
116 int y0 = flip_y_int(min(o.y * 2, sz.y - 1), sz.y);
117 int y1 = flip_y_int(min(o.y * 2 + 1, sz.y - 1), sz.y);
118 float s = chroma(ivec2(xa, y0), c) + chroma(ivec2(xb, y0), c)
119 + chroma(ivec2(xa, y1), c) + chroma(ivec2(xb, y1), c);
120 fragColor = vec4(s * 0.25, 0.0, 0.0, 1.0);
121 }
122 )_";
123#endif
124
125 // Y plane resources
126 QRhiTexture* m_yTexture{};
127 QRhiTextureRenderTarget* m_yRT{};
128 QRhiRenderPassDescriptor* m_yRP{};
129 QRhiShaderResourceBindings* m_ySRB{};
130 QRhiGraphicsPipeline* m_yPipeline{};
131 QRhiReadbackResult m_yReadback{};
132
133 // UV plane resources
134 QRhiTexture* m_uvTexture{};
135 QRhiTextureRenderTarget* m_uvRT{};
136 QRhiRenderPassDescriptor* m_uvRP{};
137 QRhiShaderResourceBindings* m_uvSRB{};
138 QRhiGraphicsPipeline* m_uvPipeline{};
139 QRhiReadbackResult m_uvReadback{};
141 // Shared
142 QRhiSampler* m_sampler{};
143 int m_width{};
144 int m_height{};
145
146 void init(
147 QRhi& rhi, const RenderState& state, QRhiTexture* inputRGBA, int width,
148 int height, const QString& colorConversion) override
149 {
150 m_width = width;
151 m_height = height;
152
153 m_sampler = rhi.newSampler(
154 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
155 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
156 m_sampler->create();
157
158 auto vertSrc = QString::fromLatin1(vertex_shader);
159
160 // Y plane setup
161 {
162 m_yTexture = rhi.newTexture(
163 QRhiTexture::R8, QSize{width, height}, 1,
164 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
165 m_yTexture->create();
166
167 m_yRT = rhi.newTextureRenderTarget({m_yTexture});
168 m_yRP = m_yRT->newCompatibleRenderPassDescriptor();
169 m_yRT->setRenderPassDescriptor(m_yRP);
170 m_yRT->create();
171
172 m_ySRB = rhi.newShaderResourceBindings();
173 m_ySRB->setBindings({
174 QRhiShaderResourceBinding::sampledTexture(
175 3, QRhiShaderResourceBinding::FragmentStage, inputRGBA, m_sampler),
176 });
177 m_ySRB->create();
178
179 auto [vs, fs] = makeShaders(
180 state, vertSrc, QString::fromLatin1(y_frag).arg(colorConversion));
181 m_yPipeline = rhi.newGraphicsPipeline();
182 m_yPipeline->setShaderStages({
183 {QRhiShaderStage::Vertex, vs},
184 {QRhiShaderStage::Fragment, fs},
185 });
186 m_yPipeline->setVertexInputLayout({});
187 m_yPipeline->setShaderResourceBindings(m_ySRB);
188 m_yPipeline->setRenderPassDescriptor(m_yRP);
189 m_yPipeline->create();
190 }
191
192 // UV plane setup
193 {
194#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
195 m_uvTexture = rhi.newTexture(
196 QRhiTexture::RG8, QSize{width / 2, height / 2}, 1,
197 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
198#else
199 // Tight RG8 GL readback needs Qt >= 6.10; use an R8 target holding the
200 // interleaved U,V bytes directly (same readback byte layout).
201 m_uvTexture = rhi.newTexture(
202 QRhiTexture::R8, QSize{width, height / 2}, 1,
203 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
204#endif
205 m_uvTexture->create();
206
207 m_uvRT = rhi.newTextureRenderTarget({m_uvTexture});
208 m_uvRP = m_uvRT->newCompatibleRenderPassDescriptor();
209 m_uvRT->setRenderPassDescriptor(m_uvRP);
210 m_uvRT->create();
211
212 m_uvSRB = rhi.newShaderResourceBindings();
213 m_uvSRB->setBindings({
214 QRhiShaderResourceBinding::sampledTexture(
215 3, QRhiShaderResourceBinding::FragmentStage, inputRGBA, m_sampler),
216 });
217 m_uvSRB->create();
218
219#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
220 const char* uv_src = uv_frag;
221#else
222 const char* uv_src = uv_frag_packed;
223#endif
224 auto [vs, fs]
225 = makeShaders(state, vertSrc, QString::fromLatin1(uv_src).arg(colorConversion));
226 m_uvPipeline = rhi.newGraphicsPipeline();
227 m_uvPipeline->setShaderStages({
228 {QRhiShaderStage::Vertex, vs},
229 {QRhiShaderStage::Fragment, fs},
230 });
231 m_uvPipeline->setVertexInputLayout({});
232 m_uvPipeline->setShaderResourceBindings(m_uvSRB);
233 m_uvPipeline->setRenderPassDescriptor(m_uvRP);
234 m_uvPipeline->create();
235 }
236 }
237
238 void exec(QRhi& rhi, QRhiCommandBuffer& cb) override
239 {
240 // Pass 1: Y plane (full resolution)
241 cb.beginPass(m_yRT, Qt::black, {0.0f, 0});
242 cb.setGraphicsPipeline(m_yPipeline);
243 cb.setShaderResources(m_ySRB);
244 cb.setViewport(QRhiViewport(0, 0, m_width, m_height));
245 cb.draw(3);
246
247 auto* yReadbackBatch = rhi.nextResourceUpdateBatch();
248 yReadbackBatch->readBackTexture(QRhiReadbackDescription{m_yTexture}, &m_yReadback);
249 cb.endPass(yReadbackBatch);
250
251 // Pass 2: UV plane (half resolution; on Qt < 6.10 the target is R8 at
252 // full width with U,V interleaved per texel)
253 cb.beginPass(m_uvRT, Qt::black, {0.0f, 0});
254 cb.setGraphicsPipeline(m_uvPipeline);
255 cb.setShaderResources(m_uvSRB);
256#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
257 cb.setViewport(QRhiViewport(0, 0, m_width / 2, m_height / 2));
258#else
259 cb.setViewport(QRhiViewport(0, 0, m_width, m_height / 2));
260#endif
261 cb.draw(3);
262
263 auto* uvReadbackBatch = rhi.nextResourceUpdateBatch();
264 uvReadbackBatch->readBackTexture(
265 QRhiReadbackDescription{m_uvTexture}, &m_uvReadback);
266 cb.endPass(uvReadbackBatch);
267 }
268
269 int planeCount() const override { return 2; }
271 const QRhiReadbackResult& readback(int plane) const override
272 {
273 return plane == 0 ? m_yReadback : m_uvReadback;
274 }
275
276 void release() override
277 {
278 delete m_uvPipeline;
279 delete m_uvSRB;
280 delete m_uvRP;
281 delete m_uvRT;
282 delete m_uvTexture;
283 delete m_yPipeline;
284 delete m_ySRB;
285 delete m_yRP;
286 delete m_yRT;
287 delete m_yTexture;
288 delete m_sampler;
289 m_uvPipeline = m_yPipeline = nullptr;
290 m_uvSRB = m_ySRB = nullptr;
291 m_uvRP = m_yRP = nullptr;
292 m_uvRT = m_yRT = nullptr;
293 m_uvTexture = m_yTexture = nullptr;
294 m_sampler = nullptr;
295 }
296};
297
298} // 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
GPU RGBA->NV12 encoder (semi-planar 4:2:0).
Definition encoders/NV12.hpp:35
void release() override
Release all GPU resources.
Definition encoders/NV12.hpp:270
void exec(QRhi &rhi, QRhiCommandBuffer &cb) override
Definition encoders/NV12.hpp:232
int planeCount() const override
Number of readback planes (1 for UYVY, 2 for NV12, 3 for I420).
Definition encoders/NV12.hpp:263
const QRhiReadbackResult & readback(int plane) const override
Get the readback result for a given plane. Valid after endOffscreenFrame.
Definition encoders/NV12.hpp:265
void init(QRhi &rhi, const RenderState &state, QRhiTexture *inputRGBA, int width, int height, const QString &colorConversion) override
Definition encoders/NV12.hpp:140