Loading...
Searching...
No Matches
YUV420Packed.hpp
1#pragma once
2#include <Gfx/Graph/encoders/GPUVideoEncoder.hpp>
3
4namespace score::gfx
5{
6
53{
55 enum class Siting
56 {
57 Centre = 0,
58 Left = 1,
59 CentreWide = 2,
60 };
61
62 enum class Layout
63 {
64 NV12,
65 I420,
66 YV12,
67 };
68
71 static constexpr const char* chroma_nv12 = R"_(
72 float chroma_byte(int b, int cr, ivec2 sz) {
73 vec2 uv = chroma_at(b >> 1, cr, sz);
74 return ((b & 1) == 0) ? uv.x : uv.y;
75 }
76 )_";
77
80 static constexpr const char* chroma_planar = R"_(
81 float chroma_byte(int b, int cr, ivec2 sz) {
82 int cw = sz.x >> 1; // bytes in one chroma plane row
83 int ch = sz.y >> 1; // rows in one chroma plane
84 // A target row is w bytes and a chroma plane row is w/2, so a target row
85 // holds EXACTLY two chroma rows. That makes the mapping shifts and
86 // compares -- no integer division, which is worth avoiding here because
87 // it would run once per chroma byte of every frame.
88 int sel = (b < cw) ? 0 : 1; // "half" is a GLSL reserved word
89 int col = b - sel * cw;
90 int gr = (cr << 1) + sel; // chroma row counted across both planes
91 bool first = gr < ch;
92 vec2 uv = chroma_at(col, first ? gr : gr - ch, sz);
93 return first ? uv.%1 : uv.%2;
94 }
95 )_";
96
97 // %1 = colorMatrixOut() shader defining convert_from_rgb(vec3)
98 // %2 = the chroma_byte() for this layout
99 static constexpr const char* frag = R"_(#version 450
100 layout(location = 0) in vec2 v_texcoord;
101 layout(location = 0) out vec4 fragColor;
102 layout(binding = 3) uniform sampler2D src_tex;
103 )_" "%1" R"_(
104
105 // Constants, so the branches on them fold away. Declared before
106 // chroma_at(), which uses SITING.
107 const int BPT = %3;
108 const int SITING = %4;
109
110 vec2 flip_y(vec2 tc) {
111 // Only OpenGL: this pass draws a hardcoded triangle in raw NDC rather than
112 // going through renderer.clipSpaceCorrMatrix.
113 //
114 // It flips the SOURCE lookup, never the target row order -- v_texcoord.y
115 // runs from 0 at the first row of the readback on both backends, which is
116 // what lets the luma/chroma split sit anywhere rather than at the midpoint.
117 #if defined(QSHADER_SPIRV) || defined(QSHADER_MSL) || defined(QSHADER_HLSL)
118 return tc;
119 #else
120 return vec2(tc.x, 1.0 - tc.y);
121 #endif
122 }
123
124 // Luma of source pixel (x, y). At an exact texel centre a Linear sampler
125 // returns that texel, so this is the plane encoder's full-size pass.
126 float luma_at(int x, int y, ivec2 sz) {
127 vec2 tc = (vec2(float(x), float(y)) + 0.5) / vec2(sz);
128 return convert_from_rgb(texture(src_tex, flip_y(tc)).rgb).x;
129 }
130
131 // (Cb, Cr) of chroma site (cx, cy). Vertically always the boundary
132 // between source rows 2*cy and 2*cy+1, so one tap averages the pair.
133 vec2 chroma_at(int cx, int cy, ivec2 sz) {
134 vec2 fs = vec2(sz);
135 float y = float((cy << 1) + 1) / fs.y; // boundary of the row pair
136 if(SITING == 0)
137 {
138 float x = float((cx << 1) + 1) / fs.x; // boundary of the column pair
139 return convert_from_rgb(texture(src_tex, flip_y(vec2(x, y))).rgb).yz;
140 }
141 if(SITING == 2)
142 {
143 // [1 3 3 1]/8 each way as four bilinear taps: a tap at t=0.75 weights
144 // its pair 1:3 and one at t=0.25 weights it 3:1, so averaging the two
145 // gives 1:3:3:1 centred on the same boundary the box uses.
146 float xa = (float(cx << 1) + 0.25) / fs.x;
147 float xb = (float(cx << 1) + 1.75) / fs.x;
148 float ya = (float(cy << 1) + 0.25) / fs.y;
149 float yb = (float(cy << 1) + 1.75) / fs.y;
150 vec3 p0 = convert_from_rgb(texture(src_tex, flip_y(vec2(xa, ya))).rgb);
151 vec3 p1 = convert_from_rgb(texture(src_tex, flip_y(vec2(xb, ya))).rgb);
152 vec3 p2 = convert_from_rgb(texture(src_tex, flip_y(vec2(xa, yb))).rgb);
153 vec3 p3 = convert_from_rgb(texture(src_tex, flip_y(vec2(xb, yb))).rgb);
154 return (p0.yz + p1.yz + p2.yz + p3.yz) * 0.25;
155 }
156 float xa = float(cx << 1) / fs.x; // left of column 2*cx
157 float xb = float((cx << 1) + 1) / fs.x; // right of it
158 vec3 a = convert_from_rgb(texture(src_tex, flip_y(vec2(xa, y))).rgb);
159 vec3 b = convert_from_rgb(texture(src_tex, flip_y(vec2(xb, y))).rgb);
160 return (a.yz + b.yz) * 0.5;
161 }
162 )_" "%2" R"_(
163
164
165 float byte_at(int b, int outRow, ivec2 sz) {
166 return (outRow < sz.y) ? luma_at(b, outRow, sz)
167 : chroma_byte(b, outRow - sz.y, sz);
168 }
169
170 void main() {
171 ivec2 sz = textureSize(src_tex, 0);
172 int outRows = sz.y + (sz.y >> 1);
173
174 int outRow = int(floor(v_texcoord.y * float(outRows)));
175 int t = int(floor(v_texcoord.x * float(sz.x / BPT)));
176 int b0 = t * BPT;
177
178 if(BPT == 1)
179 fragColor = vec4(byte_at(b0, outRow, sz), 0.0, 0.0, 1.0);
180 else
181 fragColor = vec4(
182 byte_at(b0, outRow, sz), byte_at(b0 + 1, outRow, sz),
183 byte_at(b0 + 2, outRow, sz), byte_at(b0 + 3, outRow, sz));
184 }
185 )_";
186
187 explicit Yuv420PackedEncoder(Layout layout, Siting siting = Siting::Centre) noexcept
188 : m_layout{layout}
189 , m_siting{siting}
190 {
191 }
192
193 static std::unique_ptr<Yuv420PackedEncoder> nv12()
194 {
195 return std::make_unique<Yuv420PackedEncoder>(Layout::NV12);
196 }
197 static std::unique_ptr<Yuv420PackedEncoder> i420()
198 {
199 return std::make_unique<Yuv420PackedEncoder>(Layout::I420);
200 }
201 static std::unique_ptr<Yuv420PackedEncoder> yv12()
202 {
203 return std::make_unique<Yuv420PackedEncoder>(Layout::YV12);
204 }
205
206 Layout m_layout{Layout::NV12};
207 Siting m_siting{Siting::Centre};
208 QRhiTexture* m_outTexture{};
209 QRhiTextureRenderTarget* m_renderTarget{};
210 QRhiRenderPassDescriptor* m_rpDesc{};
211 QRhiSampler* m_sampler{};
212 QRhiShaderResourceBindings* m_srb{};
213 QRhiGraphicsPipeline* m_pipeline{};
214 QRhiReadbackResult m_readback{};
215 int m_width{};
216 int m_height{};
217 int m_bytesPerTexel{1};
218 bool m_readbackEnabled{true};
219
221 int framestoreRows() const noexcept { return m_height + m_height / 2; }
222
223 void init(
224 QRhi& rhi, const RenderState& state, QRhiTexture* inputRGBA, int width,
225 int height, const QString& colorConversion) override
226 {
227 m_width = width;
228 m_height = height;
229
230 // Overridable so the two sitings can be compared against a real receiver
231 // rather than argued about. See the chroma_at() comment.
232 if(const auto env = qgetenv("SCORE_GFX_CHROMA_SITING"); !env.isEmpty())
233 {
234 const auto e = env.toLower();
235 m_siting = (e == "left") ? Siting::Left
236 : (e == "wide") ? Siting::CentreWide
237 : Siting::Centre;
238 }
239
240 m_bytesPerTexel = (width % 4 == 0) ? 4 : 1;
241 m_outTexture = rhi.newTexture(
242 m_bytesPerTexel == 4 ? QRhiTexture::RGBA8 : QRhiTexture::R8,
243 QSize{width / m_bytesPerTexel, framestoreRows()}, 1,
244 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
245 m_outTexture->create();
246
247 m_renderTarget = rhi.newTextureRenderTarget({m_outTexture});
248 m_rpDesc = m_renderTarget->newCompatibleRenderPassDescriptor();
249 m_renderTarget->setRenderPassDescriptor(m_rpDesc);
250 m_renderTarget->create();
251
252 // Linear: the chroma taps need bilinear filtering to average each block.
253 // Luma taps sit on exact texel centres, where Linear is a no-op.
254 m_sampler = rhi.newSampler(
255 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
256 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
257 m_sampler->create();
258
259 m_srb = rhi.newShaderResourceBindings();
260 m_srb->setBindings({
261 QRhiShaderResourceBinding::sampledTexture(
262 3, QRhiShaderResourceBinding::FragmentStage, inputRGBA, m_sampler),
263 });
264 m_srb->create();
265
266 QString chroma;
267 switch(m_layout)
268 {
269 case Layout::NV12:
270 chroma = QString::fromLatin1(chroma_nv12);
271 break;
272 case Layout::I420: // Cb first, then Cr
273 chroma = QString::fromLatin1(chroma_planar)
274 .arg(QStringLiteral("x"), QStringLiteral("y"));
275 break;
276 case Layout::YV12: // Cr first, then Cb
277 chroma = QString::fromLatin1(chroma_planar)
278 .arg(QStringLiteral("y"), QStringLiteral("x"));
279 break;
280 }
281
282 // One pass, so a %2 inside colorConversion cannot be eaten as a placeholder.
283 auto [vertS, fragS] = makeShaders(
284 state, QString::fromLatin1(vertex_shader),
285 QString::fromLatin1(frag).arg(
286 colorConversion, chroma, QString::number(m_bytesPerTexel),
287 QString::number(int(m_siting))));
288
289 m_pipeline = rhi.newGraphicsPipeline();
290 m_pipeline->setShaderStages({
291 {QRhiShaderStage::Vertex, vertS},
292 {QRhiShaderStage::Fragment, fragS},
293 });
294 m_pipeline->setVertexInputLayout({});
295 m_pipeline->setShaderResourceBindings(m_srb);
296 m_pipeline->setRenderPassDescriptor(m_rpDesc);
297 m_pipeline->create();
298 }
299
300 void exec(QRhi& rhi, QRhiCommandBuffer& cb) override
301 {
302 cb.beginPass(m_renderTarget, Qt::black, {0.0f, 0});
303 cb.setGraphicsPipeline(m_pipeline);
304 cb.setShaderResources(m_srb);
305 cb.setViewport(
306 QRhiViewport(0, 0, m_width / m_bytesPerTexel, framestoreRows()));
307 cb.draw(3);
308
309 if(m_readbackEnabled)
310 {
311 auto* readbackBatch = rhi.nextResourceUpdateBatch();
312 QRhiReadbackDescription rb(m_outTexture);
313 readbackBatch->readBackTexture(rb, &m_readback);
314 cb.endPass(readbackBatch);
315 }
316 else
317 {
318 cb.endPass();
320 }
322 int planeCount() const override { return 1; }
324 const QRhiReadbackResult& readback(int) const override { return m_readback; }
325 QRhiTexture* outputTexture() const noexcept override { return m_outTexture; }
326 void setReadbackEnabled(bool e) noexcept override { m_readbackEnabled = e; }
327
328 void release() override
329 {
330 delete m_pipeline;
331 m_pipeline = nullptr;
332 delete m_srb;
333 m_srb = nullptr;
334 delete m_sampler;
335 m_sampler = nullptr;
336 delete m_rpDesc;
337 m_rpDesc = nullptr;
338 delete m_renderTarget;
339 m_renderTarget = nullptr;
340 delete m_outTexture;
341 m_outTexture = nullptr;
342 }
343};
344
345} // 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:1303
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 / I420 / YV12, one texture holding the framestore.
Definition YUV420Packed.hpp:53
Layout
Definition YUV420Packed.hpp:63
@ NV12
Y, then Cb/Cr interleaved.
void setReadbackEnabled(bool e) noexcept override
Definition YUV420Packed.hpp:323
void exec(QRhi &rhi, QRhiCommandBuffer &cb) override
Definition YUV420Packed.hpp:297
static constexpr const char * chroma_planar
Definition YUV420Packed.hpp:80
int framestoreRows() const noexcept
Rows in the whole framestore: the picture, plus half of it again.
Definition YUV420Packed.hpp:218
void init(QRhi &rhi, const RenderState &state, QRhiTexture *inputRGBA, int width, int height, const QString &colorConversion) override
Definition YUV420Packed.hpp:220
Siting
Where a chroma sample sits horizontally relative to its luma pair.
Definition YUV420Packed.hpp:56
@ Centre
between the two columns: a 2x2 box, what swscale emits
@ Left
on the even column, [1 2 1]/4: what MPEG-2/H.264 specify
@ CentreWide
centre, but [1 3 3 1]/8 each way instead of a box
static constexpr const char * chroma_nv12
Definition YUV420Packed.hpp:71
void release() override
Release all GPU resources.
Definition YUV420Packed.hpp:325
int planeCount() const override
Number of readback planes (1 for UYVY, 2 for NV12, 3 for I420).
Definition YUV420Packed.hpp:319
const QRhiReadbackResult & readback(int) const override
Get the readback result for a given plane. Valid after endOffscreenFrame.
Definition YUV420Packed.hpp:321
QRhiTexture * outputTexture() const noexcept override
Definition YUV420Packed.hpp:322