Loading...
Searching...
No Matches
GeometryPacker.hpp
1#pragma once
2#include <Threedim/GeometryToBufferStrategies.hpp>
3#include <halp/buffer.hpp>
4#include <halp/meta.hpp>
5
6#include <algorithm>
7#include <array>
8
9namespace score::gfx
10{
11class RenderList;
12struct Edge;
13}
14
15namespace Threedim
16{
18{
19 halp::attribute_semantic location{};
20 bool pad_to_vec4{false};
21};
22
24{
25 int32_t src_buffer_index{-1}; // Index into mesh.buffers
26 int32_t src_stride{};
27 int32_t src_offset{}; // attribute offset + input offset
28 int32_t element_count{};
29 int32_t output_components{}; // element_count or 4 if padded
30 int32_t output_offset{}; // Offset in output stride
31 bool is_float{true};
32 bool is_active{false};
33};
34
36{
37 static constexpr int MAX_PACKED_ATTRIBUTES = 8;
38
39public:
40 bool init(
41 const score::gfx::RenderState& renderState, QRhi& rhi,
42 const halp::dynamic_gpu_geometry& mesh,
43 std::span<const packed_attribute_spec> specs)
44 {
45 if(specs.empty() || specs.size() > MAX_PACKED_ATTRIBUTES)
46 {
47 qDebug() << "PackedExtractionStrategy: Invalid attribute count:" << specs.size();
48 return false;
49 }
50
51 m_hasIndexBuffer = mesh.index.buffer >= 0;
52 m_vertexCount = m_hasIndexBuffer ? mesh.indices : mesh.vertices;
53 m_attributeCount = 0;
54 m_outputStride = 0;
55
56 // Gather unique source buffers
57 m_srcBufferCount = 0;
58 std::fill(std::begin(m_srcBufferMapping), std::end(m_srcBufferMapping), -1);
59
60 // Process each requested attribute
61 for(const auto& spec : specs)
62 {
63 auto lookup = findAttribute(mesh, spec.location);
64 if(!lookup)
65 {
66 qDebug() << "PackedExtractionStrategy: Attribute not found:"
67 << magic_enum::enum_name(spec.location);
68 continue;
69 }
70
71 auto& info = m_attributes[m_attributeCount];
72 info.is_active = true;
73 info.src_buffer_index = lookup->input->buffer;
74 info.src_stride = lookup->binding->stride;
75 info.src_offset = lookup->attribute->byte_offset
76 + static_cast<int32_t>(lookup->input->byte_offset);
77 info.element_count = attributeFormatComponents(lookup->attribute->format);
78 info.is_float = isFloatFormat(lookup->attribute->format);
79 info.output_components
80 = (spec.pad_to_vec4 && info.element_count < 4 && info.is_float)
81 ? 4
82 : info.element_count;
83 info.output_offset = m_outputStride;
84
85 m_outputStride += info.output_components * sizeof(float);
86
87 // Track unique source buffers
88 int mappedIndex = m_srcBufferMapping[info.src_buffer_index];
89 if(mappedIndex < 0)
90 {
91 if(m_srcBufferCount >= MAX_PACKED_ATTRIBUTES)
92 {
93 qDebug() << "PackedExtractionStrategy: Too many source buffers";
94 return false;
95 }
96 m_srcBufferMapping[info.src_buffer_index] = m_srcBufferCount;
97 m_srcBuffers[m_srcBufferCount]
98 = static_cast<QRhiBuffer*>(mesh.buffers[info.src_buffer_index].handle);
99 ++m_srcBufferCount;
100 }
101
102 ++m_attributeCount;
103 }
104
105 if(m_attributeCount == 0)
106 {
107 qDebug() << "PackedExtractionStrategy: No valid attributes found";
108 return false;
109 }
110
111 // Handle index buffer
112 if(m_hasIndexBuffer)
113 {
114 if(mesh.index.buffer < 0
115 || mesh.index.buffer >= static_cast<int>(mesh.buffers.size()))
116 {
117 qDebug() << "PackedExtractionStrategy: Invalid index buffer";
118 return false;
119 }
120 m_indexBuffer = static_cast<QRhiBuffer*>(mesh.buffers[mesh.index.buffer].handle);
121 m_indexOffset = static_cast<int32_t>(mesh.index.byte_offset);
122 m_indexFormat32 = (mesh.index.format == halp::index_format::uint32);
123 }
124
125 m_outputSize = static_cast<int64_t>(m_vertexCount) * m_outputStride;
126
127 if(m_outputSize == 0)
128 {
129 qDebug() << "PackedExtractionStrategy: Zero output size";
130 return false;
131 }
132
133 // Create output buffer
134 m_outputBuffer = rhi.newBuffer(
135 QRhiBuffer::Static, QRhiBuffer::StorageBuffer | QRhiBuffer::VertexBuffer,
136 static_cast<quint32>(m_outputSize));
137 m_outputBuffer->setName("GeometryPacker::m_outputBuffer");
138
139 if(!m_outputBuffer || !m_outputBuffer->create())
140 {
141 qDebug() << "PackedExtractionStrategy: Failed to create output buffer";
142 return false;
143 }
144
145 return createPipeline(renderState, rhi);
146 }
147
148 void update(
149 QRhi& rhi, const halp::dynamic_gpu_geometry& mesh,
150 std::span<const packed_attribute_spec> specs)
151 {
152 // Check if vertex count changed
153 const int32_t count = m_hasIndexBuffer ? mesh.indices : mesh.vertices;
154 const int64_t newSize = static_cast<int64_t>(count) * m_outputStride;
155
156 if(newSize != m_outputSize || count != m_vertexCount)
157 {
158 m_vertexCount = count;
159 m_outputSize = newSize;
160
161 if(m_outputSize > 0)
162 {
163 m_outputBuffer->setSize(static_cast<quint32>(m_outputSize));
164 m_outputBuffer->create();
165 }
166 }
167
168 // Update source buffer handles (they may have been recreated)
169 std::fill(std::begin(m_srcBufferMapping), std::end(m_srcBufferMapping), -1);
170 m_srcBufferCount = 0;
171
172 int attrIdx = 0;
173 for(const auto& spec : specs)
174 {
175 if(attrIdx >= m_attributeCount)
176 break;
177
178 auto lookup = findAttribute(mesh, spec.location);
179 if(!lookup)
180 continue;
181
182 auto& info = m_attributes[attrIdx];
183 info.src_buffer_index = lookup->input->buffer;
184 info.src_stride = lookup->binding->stride;
185 info.src_offset = lookup->attribute->byte_offset
186 + static_cast<int32_t>(lookup->input->byte_offset);
187
188 int mappedIndex = m_srcBufferMapping[info.src_buffer_index];
189 if(mappedIndex < 0)
190 {
191 m_srcBufferMapping[info.src_buffer_index] = m_srcBufferCount;
192 m_srcBuffers[m_srcBufferCount]
193 = static_cast<QRhiBuffer*>(mesh.buffers[info.src_buffer_index].handle);
194 ++m_srcBufferCount;
195 }
196
197 ++attrIdx;
198 }
199
200 // Update index buffer
201 if(m_hasIndexBuffer && mesh.index.buffer >= 0)
202 {
203 m_indexBuffer = static_cast<QRhiBuffer*>(mesh.buffers[mesh.index.buffer].handle);
204 m_indexOffset = static_cast<int32_t>(mesh.index.byte_offset);
205 m_indexFormat32 = (mesh.index.format == halp::index_format::uint32);
206 }
207
208 updateBindings();
209 m_dirty = true;
210 }
211
212 void release() noexcept
213 {
214 delete m_pipeline;
215 m_pipeline = nullptr;
216
217 delete m_srb;
218 m_srb = nullptr;
219
220 delete m_uniformBuffer;
221 m_uniformBuffer = nullptr;
222
223 delete m_placeholderBuffer;
224 m_placeholderBuffer = nullptr;
225 std::fill(std::begin(m_boundBuffers), std::end(m_boundBuffers), nullptr);
226 m_sourcesValid = false;
227
228 // Freed by the engine:
229 // delete m_outputBuffer;
230 // m_outputBuffer = nullptr;
231
232 std::fill(std::begin(m_srcBuffers), std::end(m_srcBuffers), nullptr);
233 m_indexBuffer = nullptr;
234 }
235
236 void runCompute(QRhi& rhi, QRhiCommandBuffer& cb, QRhiResourceUpdateBatch*& res)
237 {
238 if(!m_dirty || m_vertexCount == 0 || !m_pipeline || !m_sourcesValid)
239 return;
240
241 // Prepare UBO data
242 struct alignas(16) AttributeParams
243 {
244 uint32_t srcBufferIndex; // Which source buffer (0-7)
245 uint32_t srcStrideBytes;
246 uint32_t srcOffsetBytes;
247 uint32_t elementCount;
248 uint32_t outputComponents;
249 uint32_t outputOffsetBytes;
250 uint32_t isActive;
251 uint32_t _pad;
252 };
253
254 struct alignas(64) Params
255 {
256 uint32_t vertexCount;
257 uint32_t attributeCount;
258 uint32_t outputStrideBytes;
259 uint32_t hasIndexBuffer;
260 uint32_t indexOffsetBytes;
261 uint32_t index32Bit;
262 uint32_t _pad[2];
263 alignas(64) AttributeParams attributes[MAX_PACKED_ATTRIBUTES];
264 } params{};
265 static_assert(offsetof(Params, attributes[0]) == 64);
266
267 params.vertexCount = static_cast<uint32_t>(m_vertexCount);
268 params.attributeCount = static_cast<uint32_t>(m_attributeCount);
269 params.outputStrideBytes = static_cast<uint32_t>(m_outputStride);
270 params.hasIndexBuffer = m_hasIndexBuffer ? 1u : 0u;
271 params.indexOffsetBytes = static_cast<uint32_t>(m_indexOffset);
272 params.index32Bit = m_indexFormat32 ? 1u : 0u;
273
274 for(int i = 0; i < MAX_PACKED_ATTRIBUTES; ++i)
275 {
276 const auto& info = m_attributes[i];
277 auto& ap = params.attributes[i];
278
279 if(info.is_active)
280 {
281 ap.srcBufferIndex
282 = static_cast<uint32_t>(m_srcBufferMapping[info.src_buffer_index]);
283 ap.srcStrideBytes = static_cast<uint32_t>(info.src_stride);
284 ap.srcOffsetBytes = static_cast<uint32_t>(info.src_offset);
285 ap.elementCount = static_cast<uint32_t>(info.element_count);
286 ap.outputComponents = static_cast<uint32_t>(info.output_components);
287 ap.outputOffsetBytes = static_cast<uint32_t>(info.output_offset);
288 ap.isActive = 1u;
289 }
290 else
291 {
292 ap.isActive = 0u;
293 }
294 }
295
296 res->updateDynamicBuffer(m_uniformBuffer, 0, sizeof(params), &params);
297
298 cb.beginComputePass(res);
299 cb.setComputePipeline(m_pipeline);
300 cb.setShaderResources(m_srb);
301
302 const int workgroups = (m_vertexCount + 255) / 256;
303 cb.dispatch(workgroups, 1, 1);
304
305 cb.endComputePass();
306
307 res = rhi.nextResourceUpdateBatch();
308
309 m_dirty = false;
310 }
311
312 [[nodiscard]] gpu_buffer_view output() const noexcept
313 {
314 return {
315 .buffer = m_outputBuffer,
316 .offset = 0,
317 .size = m_outputSize,
318 };
319 }
320
321 [[nodiscard]] int32_t outputStride() const noexcept { return m_outputStride; }
322
323 [[nodiscard]] int32_t attributeCount() const noexcept { return m_attributeCount; }
324
325 [[nodiscard]] const packed_attribute_info& attributeInfo(int index) const noexcept
326 {
327 return m_attributes[index];
328 }
329
330 [[nodiscard]] static constexpr bool needsCompute() noexcept { return true; }
331
332private:
333 bool createPipeline(const score::gfx::RenderState& renderState, QRhi& rhi)
334 {
335 m_uniformBuffer = rhi.newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 512);
336 m_uniformBuffer->setName("GeometryPacker::m_uniformBuffer");
337
338 if(!m_uniformBuffer || !m_uniformBuffer->create())
339 {
340 qDebug() << "PackedExtractionStrategy: UBO creation failed";
341 return false;
342 }
343
344 static const QString shaderCode = QStringLiteral(R"(#version 450
345
346layout(local_size_x = 256) in;
347
348struct AttributeParams {
349 uint srcBufferIndex;
350 uint srcStrideBytes;
351 uint srcOffsetBytes;
352 uint elementCount;
353 uint outputComponents;
354 uint outputOffsetBytes;
355 uint isActive;
356 uint _pad;
357};
358
359layout(std140, binding = 0) uniform Params {
360 uint vertexCount;
361 uint attributeCount;
362 uint outputStrideBytes;
363 uint hasIndexBuffer;
364 uint indexOffsetBytes;
365 uint index32Bit;
366 uint _pad[2];
367 AttributeParams attributes[8];
368};
369
370layout(std430, binding = 1) readonly buffer SrcBuffer0 { uint src0[]; };
371layout(std430, binding = 2) readonly buffer SrcBuffer1 { uint src1[]; };
372layout(std430, binding = 3) readonly buffer SrcBuffer2 { uint src2[]; };
373layout(std430, binding = 4) readonly buffer SrcBuffer3 { uint src3[]; };
374layout(std430, binding = 5) readonly buffer SrcBuffer4 { uint src4[]; };
375layout(std430, binding = 6) readonly buffer SrcBuffer5 { uint src5[]; };
376layout(std430, binding = 7) readonly buffer SrcBuffer6 { uint src6[]; };
377layout(std430, binding = 8) readonly buffer SrcBuffer7 { uint src7[]; };
378
379layout(std430, binding = 9) readonly buffer IndexBuffer { uint index_data[]; };
380
381layout(std430, binding = 10) writeonly buffer DstBuffer { uint dst_data[]; };
382
383uint readSrcData(uint bufferIndex, uint wordIndex)
384{
385 switch (bufferIndex)
386 {
387 case 0: return src0[wordIndex];
388 case 1: return src1[wordIndex];
389 case 2: return src2[wordIndex];
390 case 3: return src3[wordIndex];
391 case 4: return src4[wordIndex];
392 case 5: return src5[wordIndex];
393 case 6: return src6[wordIndex];
394 case 7: return src7[wordIndex];
395 }
396 return 0;
397}
398
399uint readIndex(uint i)
400{
401 if (index32Bit != 0)
402 {
403 uint wordIndex = (indexOffsetBytes / 4) + i;
404 return index_data[wordIndex];
405 }
406 else
407 {
408 uint bytePos = indexOffsetBytes + i * 2;
409 uint wordIndex = bytePos / 4;
410 uint word = index_data[wordIndex];
411 uint shift = (bytePos % 4) * 8;
412 return (word >> shift) & 0xFFFFu;
413 }
414}
415
416void main()
417{
418 uint outputIdx = gl_GlobalInvocationID.x;
419 if (outputIdx >= vertexCount)
420 return;
421
422 // Get source vertex index (may differ from output index if indexed)
423 uint srcVertexIdx = hasIndexBuffer != 0 ? readIndex(outputIdx) : outputIdx;
424
425 // Output base position (in uints, since outputStrideBytes is in bytes)
426 uint dstBase = (outputIdx * outputStrideBytes) / 4;
427
428 // Process each attribute
429 for (uint a = 0; a < attributeCount; ++a)
430 {
431 AttributeParams attr = attributes[a];
432 if (attr.isActive == 0)
433 continue;
434
435 // Source position
436 uint srcBase = (srcVertexIdx * attr.srcStrideBytes + attr.srcOffsetBytes) / 4;
437
438 // Destination position for this attribute
439 uint attrDstBase = dstBase + (attr.outputOffsetBytes / 4);
440
441 // Copy elements
442 for (uint i = 0; i < attr.elementCount; ++i)
443 {
444 uint value = readSrcData(attr.srcBufferIndex, srcBase + i);
445 dst_data[attrDstBase + i] = value;
446 }
447
448 // Pad to vec4 if needed
449 if (attr.outputComponents > attr.elementCount)
450 {
451 for (uint i = attr.elementCount; i < attr.outputComponents; ++i)
452 {
453 // w = 1.0f (0x3f800000), others = 0
454 dst_data[attrDstBase + i] = (i == 3) ? 0x3f800000u : 0u;
455 }
456 }
457 }
458}
459)");
460
461 QShader shader = score::gfx::makeCompute(renderState, shaderCode);
462 if(!shader.isValid())
463 {
464 qDebug() << "PackedExtractionStrategy: Shader compilation failed";
465 return false;
466 }
467
468 m_placeholderBuffer = rhi.newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, 16);
469 m_placeholderBuffer->setName("GeometryPacker::m_placeholderBuffer");
470 if(!m_placeholderBuffer->create())
471 {
472 qDebug() << "PackedExtractionStrategy: Placeholder buffer creation failed";
473 return false;
474 }
475
476 m_srb = rhi.newShaderResourceBindings();
477 updateBindings();
478
479 if(!m_srb->create())
480 {
481 qDebug() << "PackedExtractionStrategy: SRB creation failed";
482 return false;
483 }
484
485 m_pipeline = rhi.newComputePipeline();
486 m_pipeline->setShaderResourceBindings(m_srb);
487 m_pipeline->setShaderStage({QRhiShaderStage::Compute, shader});
488
489 if(!m_pipeline->create())
490 {
491 qDebug() << "PackedExtractionStrategy: Pipeline creation failed";
492 return false;
493 }
494
495 m_dirty = true;
496 return true;
497 }
498
499 void updateBindings()
500 {
501 if(!m_srb)
502 return;
503
504 m_sourcesValid = m_outputBuffer != nullptr;
505 // Unused slots get the dedicated placeholder, never a live source, so a
506 // source's buffer is bound once per slot that really reads it. D3D11 also
507 // needs the sources baked as SRVs (readonly blocks, SDK Qt patch): as UAVs
508 // any buffer in two slots, the placeholder included, overlaps and the
509 // runtime drops the whole bind, output included.
510 QRhiBuffer* const placeholderBuffer = m_placeholderBuffer;
511 for(int i = 0; i < m_srcBufferCount; ++i)
512 if(!m_srcBuffers[i] || m_srcBuffers[i] == m_outputBuffer)
513 m_sourcesValid = false;
514
515 QRhiBuffer* buffers[MAX_PACKED_ATTRIBUTES + 2]{};
516 for(int i = 0; i < MAX_PACKED_ATTRIBUTES; ++i)
517 {
518 QRhiBuffer* buf = i < m_srcBufferCount ? m_srcBuffers[i] : nullptr;
519 buffers[i] = buf && buf != m_outputBuffer ? buf : placeholderBuffer;
520 }
521
522 QRhiBuffer* idxBuf = placeholderBuffer;
523 if(m_hasIndexBuffer)
524 {
525 if(m_indexBuffer && m_indexBuffer != m_outputBuffer)
526 idxBuf = m_indexBuffer;
527 else
528 m_sourcesValid = false;
529 }
530 buffers[MAX_PACKED_ATTRIBUTES] = idxBuf;
531 buffers[MAX_PACKED_ATTRIBUTES + 1] = m_outputBuffer;
532
533 if(std::equal(std::begin(buffers), std::end(buffers), std::begin(m_boundBuffers)))
534 return;
535 std::copy(std::begin(buffers), std::end(buffers), std::begin(m_boundBuffers));
536
537 QVarLengthArray<QRhiShaderResourceBinding, 12> bindings;
538
539 bindings.append(
540 QRhiShaderResourceBinding::uniformBuffer(
541 0, QRhiShaderResourceBinding::ComputeStage, m_uniformBuffer));
542
543 for(int i = 0; i < MAX_PACKED_ATTRIBUTES; ++i)
544 {
545 bindings.append(
546 QRhiShaderResourceBinding::bufferLoad(
547 1 + i, QRhiShaderResourceBinding::ComputeStage, buffers[i]));
548 }
549
550 bindings.append(
551 QRhiShaderResourceBinding::bufferLoad(
552 9, QRhiShaderResourceBinding::ComputeStage, idxBuf));
553
554 bindings.append(
555 QRhiShaderResourceBinding::bufferStore(
556 10, QRhiShaderResourceBinding::ComputeStage, m_outputBuffer));
557
558 m_srb->setBindings(bindings.cbegin(), bindings.cend());
559 if(m_pipeline)
560 m_srb->create();
561 }
562
563 // Source buffers (up to MAX_PACKED_ATTRIBUTES unique buffers)
564 QRhiBuffer* m_srcBuffers[MAX_PACKED_ATTRIBUTES]{};
565 int m_srcBufferMapping[32]{}; // mesh buffer index -> m_srcBuffers index
566 int m_srcBufferCount{};
567
568 QRhiBuffer* m_indexBuffer{};
569 QRhiBuffer* m_outputBuffer{};
570 QRhiShaderResourceBindings* m_srb{};
571 QRhiComputePipeline* m_pipeline{};
572 QRhiBuffer* m_uniformBuffer{};
573 QRhiBuffer* m_placeholderBuffer{};
574 QRhiBuffer* m_boundBuffers[MAX_PACKED_ATTRIBUTES + 2]{};
575 bool m_sourcesValid{false};
576
577 packed_attribute_info m_attributes[MAX_PACKED_ATTRIBUTES]{};
578 int32_t m_attributeCount{};
579 int32_t m_vertexCount{};
580 int32_t m_outputStride{}; // In bytes
581 int32_t m_indexOffset{};
582 int64_t m_outputSize{};
583
584 bool m_hasIndexBuffer{false};
585 bool m_indexFormat32{true};
586 bool m_dirty{true};
587};
588
590{
591public:
592 halp_meta(name, "Repack attributes")
593 halp_meta(category, "Visuals/Utilities")
594 halp_meta(c_name, "pack_geometry")
595 halp_meta(description, "Repack selected geometry attributes into a GPU buffer and report its vertex stride.")
596 halp_meta(manual_url, "https://ossia.io/score-docs/processes/repack-attributes.html")
597 halp_meta(uuid, "7d7d5973-4aa9-4bfe-9249-8b892d92e0db")
598
599 enum class Attribute3
600 {
601 None,
602 Vec3,
603 Vec4
604 };
605 enum class Attribute2
606 {
607 None,
608 Vec2
609 };
610
611 struct ins
612 {
613 struct
614 {
615 halp_meta(name, "Geometry");
616 halp::dynamic_gpu_geometry mesh;
617 float transform[16]{};
618 bool dirty_mesh = false;
619 bool dirty_transform = false;
620 } geometry;
621
622 halp::combobox_t<"Position", Attribute3> position;
623 halp::combobox_t<"Normal", Attribute3> normal;
624 halp::combobox_t<"Color", Attribute3> color;
625 halp::combobox_t<"TexCoord", Attribute2> texcoord;
626 halp::combobox_t<"Tangent", Attribute3> tangent;
627 } inputs;
628
629 struct outs
630 {
631 halp::gpu_buffer_output<"Buffer"> buffer;
632
633 struct
634 {
635 halp_meta(name, "Stride");
636 int32_t value{};
637 } stride;
638 } outputs;
639
640 GeometryPacker() = default;
641 ~GeometryPacker() = default;
642
643 void init(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res);
644
645 void update(
646 score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res,
648
649 void release(score::gfx::RenderList& r);
650
651 void runInitialPasses(
652 score::gfx::RenderList& renderer, QRhiCommandBuffer& commands,
653 QRhiResourceUpdateBatch*& res, score::gfx::Edge& edge);
654
655 void operator()() { }
656
657private:
658 void buildAttributeSpecs();
659 bool specsChanged() const;
660
661 PackedExtractionStrategy m_strategy;
662 std::vector<packed_attribute_spec> m_specs;
663 std::vector<packed_attribute_spec> m_lastSpecs;
664 bool m_initialized{false};
665};
666
667}
Definition GeometryPacker.hpp:590
Definition GeometryPacker.hpp:36
List of nodes to be rendered to an output.
Definition RenderList.hpp:33
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
QShader makeCompute(const RenderState &v, QString compute)
Compile a compute shader.
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:1622
Definition GeometryPacker.hpp:612
Definition GeometryPacker.hpp:630
Definition GeometryToBufferStrategies.hpp:246
Definition TinyObj.hpp:27
Definition GeometryPacker.hpp:24
Definition GeometryPacker.hpp:18
Connection between two score::gfx::Port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:236
Global state associated to a rendering context.
Definition RenderState.hpp:40