Merge pull request #2441 from ReinUsesLisp/al2p
shader: Implement AL2P and ALD.PHYS
This commit is contained in:
commit
d49efbfb4a
|
@ -59,6 +59,7 @@ public:
|
||||||
static constexpr std::size_t NumCBData = 16;
|
static constexpr std::size_t NumCBData = 16;
|
||||||
static constexpr std::size_t NumVertexArrays = 32;
|
static constexpr std::size_t NumVertexArrays = 32;
|
||||||
static constexpr std::size_t NumVertexAttributes = 32;
|
static constexpr std::size_t NumVertexAttributes = 32;
|
||||||
|
static constexpr std::size_t NumVaryings = 31;
|
||||||
static constexpr std::size_t NumTextureSamplers = 32;
|
static constexpr std::size_t NumTextureSamplers = 32;
|
||||||
static constexpr std::size_t NumClipDistances = 8;
|
static constexpr std::size_t NumClipDistances = 8;
|
||||||
static constexpr std::size_t MaxShaderProgram = 6;
|
static constexpr std::size_t MaxShaderProgram = 6;
|
||||||
|
|
|
@ -98,6 +98,10 @@ union Attribute {
|
||||||
BitField<22, 2, u64> element;
|
BitField<22, 2, u64> element;
|
||||||
BitField<24, 6, Index> index;
|
BitField<24, 6, Index> index;
|
||||||
BitField<47, 3, AttributeSize> size;
|
BitField<47, 3, AttributeSize> size;
|
||||||
|
|
||||||
|
bool IsPhysical() const {
|
||||||
|
return element == 0 && static_cast<u64>(index.Value()) == 0;
|
||||||
|
}
|
||||||
} fmt20;
|
} fmt20;
|
||||||
|
|
||||||
union {
|
union {
|
||||||
|
@ -499,6 +503,11 @@ enum class SystemVariable : u64 {
|
||||||
CircularQueueEntryAddressHigh = 0x63,
|
CircularQueueEntryAddressHigh = 0x63,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class PhysicalAttributeDirection : u64 {
|
||||||
|
Input = 0,
|
||||||
|
Output = 1,
|
||||||
|
};
|
||||||
|
|
||||||
union Instruction {
|
union Instruction {
|
||||||
Instruction& operator=(const Instruction& instr) {
|
Instruction& operator=(const Instruction& instr) {
|
||||||
value = instr.value;
|
value = instr.value;
|
||||||
|
@ -587,6 +596,7 @@ union Instruction {
|
||||||
} alu;
|
} alu;
|
||||||
|
|
||||||
union {
|
union {
|
||||||
|
BitField<38, 1, u64> idx;
|
||||||
BitField<51, 1, u64> saturate;
|
BitField<51, 1, u64> saturate;
|
||||||
BitField<52, 2, IpaSampleMode> sample_mode;
|
BitField<52, 2, IpaSampleMode> sample_mode;
|
||||||
BitField<54, 2, IpaInterpMode> interp_mode;
|
BitField<54, 2, IpaInterpMode> interp_mode;
|
||||||
|
@ -811,6 +821,12 @@ union Instruction {
|
||||||
BitField<20, 24, s64> immediate_offset;
|
BitField<20, 24, s64> immediate_offset;
|
||||||
} stg;
|
} stg;
|
||||||
|
|
||||||
|
union {
|
||||||
|
BitField<32, 1, PhysicalAttributeDirection> direction;
|
||||||
|
BitField<47, 3, AttributeSize> size;
|
||||||
|
BitField<20, 11, u64> address;
|
||||||
|
} al2p;
|
||||||
|
|
||||||
union {
|
union {
|
||||||
BitField<0, 3, u64> pred0;
|
BitField<0, 3, u64> pred0;
|
||||||
BitField<3, 3, u64> pred3;
|
BitField<3, 3, u64> pred3;
|
||||||
|
@ -1376,6 +1392,7 @@ public:
|
||||||
ST_S,
|
ST_S,
|
||||||
LDG, // Load from global memory
|
LDG, // Load from global memory
|
||||||
STG, // Store in global memory
|
STG, // Store in global memory
|
||||||
|
AL2P, // Transforms attribute memory into physical memory
|
||||||
TEX,
|
TEX,
|
||||||
TEX_B, // Texture Load Bindless
|
TEX_B, // Texture Load Bindless
|
||||||
TXQ, // Texture Query
|
TXQ, // Texture Query
|
||||||
|
@ -1646,6 +1663,7 @@ private:
|
||||||
INST("1110111101010---", Id::ST_L, Type::Memory, "ST_L"),
|
INST("1110111101010---", Id::ST_L, Type::Memory, "ST_L"),
|
||||||
INST("1110111011010---", Id::LDG, Type::Memory, "LDG"),
|
INST("1110111011010---", Id::LDG, Type::Memory, "LDG"),
|
||||||
INST("1110111011011---", Id::STG, Type::Memory, "STG"),
|
INST("1110111011011---", Id::STG, Type::Memory, "STG"),
|
||||||
|
INST("1110111110100---", Id::AL2P, Type::Memory, "AL2P"),
|
||||||
INST("110000----111---", Id::TEX, Type::Texture, "TEX"),
|
INST("110000----111---", Id::TEX, Type::Texture, "TEX"),
|
||||||
INST("1101111010111---", Id::TEX_B, Type::Texture, "TEX_B"),
|
INST("1101111010111---", Id::TEX_B, Type::Texture, "TEX_B"),
|
||||||
INST("1101111101001---", Id::TXQ, Type::Texture, "TXQ"),
|
INST("1101111101001---", Id::TXQ, Type::Texture, "TXQ"),
|
||||||
|
|
|
@ -21,9 +21,18 @@ T GetInteger(GLenum pname) {
|
||||||
|
|
||||||
Device::Device() {
|
Device::Device() {
|
||||||
uniform_buffer_alignment = GetInteger<std::size_t>(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT);
|
uniform_buffer_alignment = GetInteger<std::size_t>(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT);
|
||||||
|
max_vertex_attributes = GetInteger<u32>(GL_MAX_VERTEX_ATTRIBS);
|
||||||
|
max_varyings = GetInteger<u32>(GL_MAX_VARYING_VECTORS);
|
||||||
has_variable_aoffi = TestVariableAoffi();
|
has_variable_aoffi = TestVariableAoffi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Device::Device(std::nullptr_t) {
|
||||||
|
uniform_buffer_alignment = 0;
|
||||||
|
max_vertex_attributes = 16;
|
||||||
|
max_varyings = 15;
|
||||||
|
has_variable_aoffi = true;
|
||||||
|
}
|
||||||
|
|
||||||
bool Device::TestVariableAoffi() {
|
bool Device::TestVariableAoffi() {
|
||||||
const GLchar* AOFFI_TEST = R"(#version 430 core
|
const GLchar* AOFFI_TEST = R"(#version 430 core
|
||||||
uniform sampler2D tex;
|
uniform sampler2D tex;
|
||||||
|
|
|
@ -5,17 +5,27 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
class Device {
|
class Device {
|
||||||
public:
|
public:
|
||||||
Device();
|
explicit Device();
|
||||||
|
explicit Device(std::nullptr_t);
|
||||||
|
|
||||||
std::size_t GetUniformBufferAlignment() const {
|
std::size_t GetUniformBufferAlignment() const {
|
||||||
return uniform_buffer_alignment;
|
return uniform_buffer_alignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u32 GetMaxVertexAttributes() const {
|
||||||
|
return max_vertex_attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 GetMaxVaryings() const {
|
||||||
|
return max_varyings;
|
||||||
|
}
|
||||||
|
|
||||||
bool HasVariableAoffi() const {
|
bool HasVariableAoffi() const {
|
||||||
return has_variable_aoffi;
|
return has_variable_aoffi;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +34,8 @@ private:
|
||||||
static bool TestVariableAoffi();
|
static bool TestVariableAoffi();
|
||||||
|
|
||||||
std::size_t uniform_buffer_alignment{};
|
std::size_t uniform_buffer_alignment{};
|
||||||
|
u32 max_vertex_attributes{};
|
||||||
|
u32 max_varyings{};
|
||||||
bool has_variable_aoffi{};
|
bool has_variable_aoffi{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -134,6 +134,19 @@ bool IsPrecise(Node node) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr bool IsGenericAttribute(Attribute::Index index) {
|
||||||
|
return index >= Attribute::Index::Attribute_0 && index <= Attribute::Index::Attribute_31;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Attribute::Index ToGenericAttribute(u32 value) {
|
||||||
|
return static_cast<Attribute::Index>(value + static_cast<u32>(Attribute::Index::Attribute_0));
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 GetGenericAttributeIndex(Attribute::Index index) {
|
||||||
|
ASSERT(IsGenericAttribute(index));
|
||||||
|
return static_cast<u32>(index) - static_cast<u32>(Attribute::Index::Attribute_0);
|
||||||
|
}
|
||||||
|
|
||||||
class GLSLDecompiler final {
|
class GLSLDecompiler final {
|
||||||
public:
|
public:
|
||||||
explicit GLSLDecompiler(const Device& device, const ShaderIR& ir, ShaderStage stage,
|
explicit GLSLDecompiler(const Device& device, const ShaderIR& ir, ShaderStage stage,
|
||||||
|
@ -152,6 +165,7 @@ public:
|
||||||
DeclareConstantBuffers();
|
DeclareConstantBuffers();
|
||||||
DeclareGlobalMemory();
|
DeclareGlobalMemory();
|
||||||
DeclareSamplers();
|
DeclareSamplers();
|
||||||
|
DeclarePhysicalAttributeReader();
|
||||||
|
|
||||||
code.AddLine("void execute_" + suffix + "() {");
|
code.AddLine("void execute_" + suffix + "() {");
|
||||||
++code.scope;
|
++code.scope;
|
||||||
|
@ -296,76 +310,95 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetInputFlags(AttributeUse attribute) {
|
std::string GetInputFlags(AttributeUse attribute) {
|
||||||
std::string out;
|
|
||||||
|
|
||||||
switch (attribute) {
|
switch (attribute) {
|
||||||
case AttributeUse::Constant:
|
|
||||||
out += "flat ";
|
|
||||||
break;
|
|
||||||
case AttributeUse::ScreenLinear:
|
|
||||||
out += "noperspective ";
|
|
||||||
break;
|
|
||||||
case AttributeUse::Perspective:
|
case AttributeUse::Perspective:
|
||||||
// Default, Smooth
|
// Default, Smooth
|
||||||
break;
|
return {};
|
||||||
|
case AttributeUse::Constant:
|
||||||
|
return "flat ";
|
||||||
|
case AttributeUse::ScreenLinear:
|
||||||
|
return "noperspective ";
|
||||||
default:
|
default:
|
||||||
LOG_CRITICAL(HW_GPU, "Unused attribute being fetched");
|
case AttributeUse::Unused:
|
||||||
UNREACHABLE();
|
UNREACHABLE_MSG("Unused attribute being fetched");
|
||||||
|
return {};
|
||||||
|
UNIMPLEMENTED_MSG("Unknown attribute usage index={}", static_cast<u32>(attribute));
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeclareInputAttributes() {
|
void DeclareInputAttributes() {
|
||||||
|
if (ir.HasPhysicalAttributes()) {
|
||||||
|
const u32 num_inputs{GetNumPhysicalInputAttributes()};
|
||||||
|
for (u32 i = 0; i < num_inputs; ++i) {
|
||||||
|
DeclareInputAttribute(ToGenericAttribute(i), true);
|
||||||
|
}
|
||||||
|
code.AddNewLine();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& attributes = ir.GetInputAttributes();
|
const auto& attributes = ir.GetInputAttributes();
|
||||||
for (const auto element : attributes) {
|
for (const auto index : attributes) {
|
||||||
const Attribute::Index index = element.first;
|
if (IsGenericAttribute(index)) {
|
||||||
if (index < Attribute::Index::Attribute_0 || index > Attribute::Index::Attribute_31) {
|
DeclareInputAttribute(index, false);
|
||||||
// Skip when it's not a generic attribute
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(bunnei): Use proper number of elements for these
|
|
||||||
u32 idx = static_cast<u32>(index) - static_cast<u32>(Attribute::Index::Attribute_0);
|
|
||||||
if (stage != ShaderStage::Vertex) {
|
|
||||||
// If inputs are varyings, add an offset
|
|
||||||
idx += GENERIC_VARYING_START_LOCATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string attr = GetInputAttribute(index);
|
|
||||||
if (stage == ShaderStage::Geometry) {
|
|
||||||
attr = "gs_" + attr + "[]";
|
|
||||||
}
|
|
||||||
std::string suffix;
|
|
||||||
if (stage == ShaderStage::Fragment) {
|
|
||||||
const auto input_mode =
|
|
||||||
header.ps.GetAttributeUse(idx - GENERIC_VARYING_START_LOCATION);
|
|
||||||
suffix = GetInputFlags(input_mode);
|
|
||||||
}
|
|
||||||
code.AddLine("layout (location = " + std::to_string(idx) + ") " + suffix + "in vec4 " +
|
|
||||||
attr + ';');
|
|
||||||
}
|
}
|
||||||
if (!attributes.empty())
|
if (!attributes.empty())
|
||||||
code.AddNewLine();
|
code.AddNewLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeclareInputAttribute(Attribute::Index index, bool skip_unused) {
|
||||||
|
const u32 generic_index{GetGenericAttributeIndex(index)};
|
||||||
|
|
||||||
|
std::string name{GetInputAttribute(index)};
|
||||||
|
if (stage == ShaderStage::Geometry) {
|
||||||
|
name = "gs_" + name + "[]";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string suffix;
|
||||||
|
if (stage == ShaderStage::Fragment) {
|
||||||
|
const auto input_mode{header.ps.GetAttributeUse(generic_index)};
|
||||||
|
if (skip_unused && input_mode == AttributeUse::Unused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
suffix = GetInputFlags(input_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 location = generic_index;
|
||||||
|
if (stage != ShaderStage::Vertex) {
|
||||||
|
// If inputs are varyings, add an offset
|
||||||
|
location += GENERIC_VARYING_START_LOCATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.AddLine("layout (location = " + std::to_string(location) + ") " + suffix + "in vec4 " +
|
||||||
|
name + ';');
|
||||||
|
}
|
||||||
|
|
||||||
void DeclareOutputAttributes() {
|
void DeclareOutputAttributes() {
|
||||||
|
if (ir.HasPhysicalAttributes() && stage != ShaderStage::Fragment) {
|
||||||
|
for (u32 i = 0; i < GetNumPhysicalVaryings(); ++i) {
|
||||||
|
DeclareOutputAttribute(ToGenericAttribute(i));
|
||||||
|
}
|
||||||
|
code.AddNewLine();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& attributes = ir.GetOutputAttributes();
|
const auto& attributes = ir.GetOutputAttributes();
|
||||||
for (const auto index : attributes) {
|
for (const auto index : attributes) {
|
||||||
if (index < Attribute::Index::Attribute_0 || index > Attribute::Index::Attribute_31) {
|
if (IsGenericAttribute(index)) {
|
||||||
// Skip when it's not a generic attribute
|
DeclareOutputAttribute(index);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
// TODO(bunnei): Use proper number of elements for these
|
|
||||||
const auto idx = static_cast<u32>(index) -
|
|
||||||
static_cast<u32>(Attribute::Index::Attribute_0) +
|
|
||||||
GENERIC_VARYING_START_LOCATION;
|
|
||||||
code.AddLine("layout (location = " + std::to_string(idx) + ") out vec4 " +
|
|
||||||
GetOutputAttribute(index) + ';');
|
|
||||||
}
|
}
|
||||||
if (!attributes.empty())
|
if (!attributes.empty())
|
||||||
code.AddNewLine();
|
code.AddNewLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeclareOutputAttribute(Attribute::Index index) {
|
||||||
|
const u32 location{GetGenericAttributeIndex(index) + GENERIC_VARYING_START_LOCATION};
|
||||||
|
code.AddLine("layout (location = " + std::to_string(location) + ") out vec4 " +
|
||||||
|
GetOutputAttribute(index) + ';');
|
||||||
|
}
|
||||||
|
|
||||||
void DeclareConstantBuffers() {
|
void DeclareConstantBuffers() {
|
||||||
for (const auto& entry : ir.GetConstantBuffers()) {
|
for (const auto& entry : ir.GetConstantBuffers()) {
|
||||||
const auto [index, size] = entry;
|
const auto [index, size] = entry;
|
||||||
|
@ -429,6 +462,39 @@ private:
|
||||||
code.AddNewLine();
|
code.AddNewLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeclarePhysicalAttributeReader() {
|
||||||
|
if (!ir.HasPhysicalAttributes()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
code.AddLine("float readPhysicalAttribute(uint physical_address) {");
|
||||||
|
++code.scope;
|
||||||
|
code.AddLine("switch (physical_address) {");
|
||||||
|
|
||||||
|
// Just declare generic attributes for now.
|
||||||
|
const auto num_attributes{static_cast<u32>(GetNumPhysicalInputAttributes())};
|
||||||
|
for (u32 index = 0; index < num_attributes; ++index) {
|
||||||
|
const auto attribute{ToGenericAttribute(index)};
|
||||||
|
for (u32 element = 0; element < 4; ++element) {
|
||||||
|
constexpr u32 generic_base{0x80};
|
||||||
|
constexpr u32 generic_stride{16};
|
||||||
|
constexpr u32 element_stride{4};
|
||||||
|
const u32 address{generic_base + index * generic_stride + element * element_stride};
|
||||||
|
|
||||||
|
const bool declared{stage != ShaderStage::Fragment ||
|
||||||
|
header.ps.GetAttributeUse(index) != AttributeUse::Unused};
|
||||||
|
const std::string value{declared ? ReadAttribute(attribute, element) : "0"};
|
||||||
|
code.AddLine(fmt::format("case 0x{:x}: return {};", address, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code.AddLine("default: return 0;");
|
||||||
|
|
||||||
|
code.AddLine('}');
|
||||||
|
--code.scope;
|
||||||
|
code.AddLine('}');
|
||||||
|
code.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
void VisitBlock(const NodeBlock& bb) {
|
void VisitBlock(const NodeBlock& bb) {
|
||||||
for (const Node node : bb) {
|
for (const Node node : bb) {
|
||||||
if (const std::string expr = Visit(node); !expr.empty()) {
|
if (const std::string expr = Visit(node); !expr.empty()) {
|
||||||
|
@ -483,70 +549,12 @@ private:
|
||||||
return value;
|
return value;
|
||||||
|
|
||||||
} else if (const auto abuf = std::get_if<AbufNode>(node)) {
|
} else if (const auto abuf = std::get_if<AbufNode>(node)) {
|
||||||
const auto attribute = abuf->GetIndex();
|
UNIMPLEMENTED_IF_MSG(abuf->IsPhysicalBuffer() && stage == ShaderStage::Geometry,
|
||||||
const auto element = abuf->GetElement();
|
"Physical attributes in geometry shaders are not implemented");
|
||||||
|
if (abuf->IsPhysicalBuffer()) {
|
||||||
const auto GeometryPass = [&](const std::string& name) {
|
return "readPhysicalAttribute(ftou(" + Visit(abuf->GetPhysicalAddress()) + "))";
|
||||||
if (stage == ShaderStage::Geometry && abuf->GetBuffer()) {
|
|
||||||
// TODO(Rodrigo): Guard geometry inputs against out of bound reads. Some games
|
|
||||||
// set an 0x80000000 index for those and the shader fails to build. Find out why
|
|
||||||
// this happens and what's its intent.
|
|
||||||
return "gs_" + name + "[ftou(" + Visit(abuf->GetBuffer()) +
|
|
||||||
") % MAX_VERTEX_INPUT]";
|
|
||||||
}
|
}
|
||||||
return name;
|
return ReadAttribute(abuf->GetIndex(), abuf->GetElement(), abuf->GetBuffer());
|
||||||
};
|
|
||||||
|
|
||||||
switch (attribute) {
|
|
||||||
case Attribute::Index::Position:
|
|
||||||
if (stage != ShaderStage::Fragment) {
|
|
||||||
return GeometryPass("position") + GetSwizzle(element);
|
|
||||||
} else {
|
|
||||||
return element == 3 ? "1.0f" : "gl_FragCoord" + GetSwizzle(element);
|
|
||||||
}
|
|
||||||
case Attribute::Index::PointCoord:
|
|
||||||
switch (element) {
|
|
||||||
case 0:
|
|
||||||
return "gl_PointCoord.x";
|
|
||||||
case 1:
|
|
||||||
return "gl_PointCoord.y";
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
return "0";
|
|
||||||
}
|
|
||||||
UNREACHABLE();
|
|
||||||
return "0";
|
|
||||||
case Attribute::Index::TessCoordInstanceIDVertexID:
|
|
||||||
// TODO(Subv): Find out what the values are for the first two elements when inside a
|
|
||||||
// vertex shader, and what's the value of the fourth element when inside a Tess Eval
|
|
||||||
// shader.
|
|
||||||
ASSERT(stage == ShaderStage::Vertex);
|
|
||||||
switch (element) {
|
|
||||||
case 2:
|
|
||||||
// Config pack's first value is instance_id.
|
|
||||||
return "uintBitsToFloat(config_pack[0])";
|
|
||||||
case 3:
|
|
||||||
return "uintBitsToFloat(gl_VertexID)";
|
|
||||||
}
|
|
||||||
UNIMPLEMENTED_MSG("Unmanaged TessCoordInstanceIDVertexID element={}", element);
|
|
||||||
return "0";
|
|
||||||
case Attribute::Index::FrontFacing:
|
|
||||||
// TODO(Subv): Find out what the values are for the other elements.
|
|
||||||
ASSERT(stage == ShaderStage::Fragment);
|
|
||||||
switch (element) {
|
|
||||||
case 3:
|
|
||||||
return "itof(gl_FrontFacing ? -1 : 0)";
|
|
||||||
}
|
|
||||||
UNIMPLEMENTED_MSG("Unmanaged FrontFacing element={}", element);
|
|
||||||
return "0";
|
|
||||||
default:
|
|
||||||
if (attribute >= Attribute::Index::Attribute_0 &&
|
|
||||||
attribute <= Attribute::Index::Attribute_31) {
|
|
||||||
return GeometryPass(GetInputAttribute(attribute)) + GetSwizzle(element);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
UNIMPLEMENTED_MSG("Unhandled input attribute: {}", static_cast<u32>(attribute));
|
|
||||||
|
|
||||||
} else if (const auto cbuf = std::get_if<CbufNode>(node)) {
|
} else if (const auto cbuf = std::get_if<CbufNode>(node)) {
|
||||||
const Node offset = cbuf->GetOffset();
|
const Node offset = cbuf->GetOffset();
|
||||||
|
@ -598,6 +606,69 @@ private:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string ReadAttribute(Attribute::Index attribute, u32 element, Node buffer = {}) {
|
||||||
|
const auto GeometryPass = [&](std::string name) {
|
||||||
|
if (stage == ShaderStage::Geometry && buffer) {
|
||||||
|
// TODO(Rodrigo): Guard geometry inputs against out of bound reads. Some games
|
||||||
|
// set an 0x80000000 index for those and the shader fails to build. Find out why
|
||||||
|
// this happens and what's its intent.
|
||||||
|
return "gs_" + std::move(name) + "[ftou(" + Visit(buffer) + ") % MAX_VERTEX_INPUT]";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (attribute) {
|
||||||
|
case Attribute::Index::Position:
|
||||||
|
if (stage != ShaderStage::Fragment) {
|
||||||
|
return GeometryPass("position") + GetSwizzle(element);
|
||||||
|
} else {
|
||||||
|
return element == 3 ? "1.0f" : "gl_FragCoord" + GetSwizzle(element);
|
||||||
|
}
|
||||||
|
case Attribute::Index::PointCoord:
|
||||||
|
switch (element) {
|
||||||
|
case 0:
|
||||||
|
return "gl_PointCoord.x";
|
||||||
|
case 1:
|
||||||
|
return "gl_PointCoord.y";
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
UNREACHABLE();
|
||||||
|
return "0";
|
||||||
|
case Attribute::Index::TessCoordInstanceIDVertexID:
|
||||||
|
// TODO(Subv): Find out what the values are for the first two elements when inside a
|
||||||
|
// vertex shader, and what's the value of the fourth element when inside a Tess Eval
|
||||||
|
// shader.
|
||||||
|
ASSERT(stage == ShaderStage::Vertex);
|
||||||
|
switch (element) {
|
||||||
|
case 2:
|
||||||
|
// Config pack's first value is instance_id.
|
||||||
|
return "uintBitsToFloat(config_pack[0])";
|
||||||
|
case 3:
|
||||||
|
return "uintBitsToFloat(gl_VertexID)";
|
||||||
|
}
|
||||||
|
UNIMPLEMENTED_MSG("Unmanaged TessCoordInstanceIDVertexID element={}", element);
|
||||||
|
return "0";
|
||||||
|
case Attribute::Index::FrontFacing:
|
||||||
|
// TODO(Subv): Find out what the values are for the other elements.
|
||||||
|
ASSERT(stage == ShaderStage::Fragment);
|
||||||
|
switch (element) {
|
||||||
|
case 3:
|
||||||
|
return "itof(gl_FrontFacing ? -1 : 0)";
|
||||||
|
}
|
||||||
|
UNIMPLEMENTED_MSG("Unmanaged FrontFacing element={}", element);
|
||||||
|
return "0";
|
||||||
|
default:
|
||||||
|
if (IsGenericAttribute(attribute)) {
|
||||||
|
return GeometryPass(GetInputAttribute(attribute)) + GetSwizzle(element);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
UNIMPLEMENTED_MSG("Unhandled input attribute: {}", static_cast<u32>(attribute));
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
std::string ApplyPrecise(Operation operation, const std::string& value) {
|
std::string ApplyPrecise(Operation operation, const std::string& value) {
|
||||||
if (!IsPrecise(operation)) {
|
if (!IsPrecise(operation)) {
|
||||||
return value;
|
return value;
|
||||||
|
@ -833,6 +904,8 @@ private:
|
||||||
target = GetRegister(gpr->GetIndex());
|
target = GetRegister(gpr->GetIndex());
|
||||||
|
|
||||||
} else if (const auto abuf = std::get_if<AbufNode>(dest)) {
|
} else if (const auto abuf = std::get_if<AbufNode>(dest)) {
|
||||||
|
UNIMPLEMENTED_IF(abuf->IsPhysicalBuffer());
|
||||||
|
|
||||||
target = [&]() -> std::string {
|
target = [&]() -> std::string {
|
||||||
switch (const auto attribute = abuf->GetIndex(); abuf->GetIndex()) {
|
switch (const auto attribute = abuf->GetIndex(); abuf->GetIndex()) {
|
||||||
case Attribute::Index::Position:
|
case Attribute::Index::Position:
|
||||||
|
@ -844,8 +917,7 @@ private:
|
||||||
case Attribute::Index::ClipDistances4567:
|
case Attribute::Index::ClipDistances4567:
|
||||||
return "gl_ClipDistance[" + std::to_string(abuf->GetElement() + 4) + ']';
|
return "gl_ClipDistance[" + std::to_string(abuf->GetElement() + 4) + ']';
|
||||||
default:
|
default:
|
||||||
if (attribute >= Attribute::Index::Attribute_0 &&
|
if (IsGenericAttribute(attribute)) {
|
||||||
attribute <= Attribute::Index::Attribute_31) {
|
|
||||||
return GetOutputAttribute(attribute) + GetSwizzle(abuf->GetElement());
|
return GetOutputAttribute(attribute) + GetSwizzle(abuf->GetElement());
|
||||||
}
|
}
|
||||||
UNIMPLEMENTED_MSG("Unhandled output attribute: {}",
|
UNIMPLEMENTED_MSG("Unhandled output attribute: {}",
|
||||||
|
@ -1591,15 +1663,11 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetInputAttribute(Attribute::Index attribute) const {
|
std::string GetInputAttribute(Attribute::Index attribute) const {
|
||||||
const auto index{static_cast<u32>(attribute) -
|
return GetDeclarationWithSuffix(GetGenericAttributeIndex(attribute), "input_attr");
|
||||||
static_cast<u32>(Attribute::Index::Attribute_0)};
|
|
||||||
return GetDeclarationWithSuffix(index, "input_attr");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetOutputAttribute(Attribute::Index attribute) const {
|
std::string GetOutputAttribute(Attribute::Index attribute) const {
|
||||||
const auto index{static_cast<u32>(attribute) -
|
return GetDeclarationWithSuffix(GetGenericAttributeIndex(attribute), "output_attr");
|
||||||
static_cast<u32>(Attribute::Index::Attribute_0)};
|
|
||||||
return GetDeclarationWithSuffix(index, "output_attr");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetConstBuffer(u32 index) const {
|
std::string GetConstBuffer(u32 index) const {
|
||||||
|
@ -1640,6 +1708,19 @@ private:
|
||||||
return name + '_' + std::to_string(index) + '_' + suffix;
|
return name + '_' + std::to_string(index) + '_' + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u32 GetNumPhysicalInputAttributes() const {
|
||||||
|
return stage == ShaderStage::Vertex ? GetNumPhysicalAttributes() : GetNumPhysicalVaryings();
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 GetNumPhysicalAttributes() const {
|
||||||
|
return std::min<u32>(device.GetMaxVertexAttributes(), Maxwell::NumVertexAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 GetNumPhysicalVaryings() const {
|
||||||
|
return std::min<u32>(device.GetMaxVaryings() - GENERIC_VARYING_START_LOCATION,
|
||||||
|
Maxwell::NumVaryings);
|
||||||
|
}
|
||||||
|
|
||||||
const Device& device;
|
const Device& device;
|
||||||
const ShaderIR& ir;
|
const ShaderIR& ir;
|
||||||
const ShaderStage stage;
|
const ShaderStage stage;
|
||||||
|
|
|
@ -194,8 +194,8 @@ public:
|
||||||
for (const auto& sampler : ir.GetSamplers()) {
|
for (const auto& sampler : ir.GetSamplers()) {
|
||||||
entries.samplers.emplace_back(sampler);
|
entries.samplers.emplace_back(sampler);
|
||||||
}
|
}
|
||||||
for (const auto& attr : ir.GetInputAttributes()) {
|
for (const auto& attribute : ir.GetInputAttributes()) {
|
||||||
entries.attributes.insert(GetGenericAttributeLocation(attr.first));
|
entries.attributes.insert(GetGenericAttributeLocation(attribute));
|
||||||
}
|
}
|
||||||
entries.clip_distances = ir.GetClipDistances();
|
entries.clip_distances = ir.GetClipDistances();
|
||||||
entries.shader_length = ir.GetLength();
|
entries.shader_length = ir.GetLength();
|
||||||
|
@ -321,8 +321,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeclareInputAttributes() {
|
void DeclareInputAttributes() {
|
||||||
for (const auto element : ir.GetInputAttributes()) {
|
for (const auto index : ir.GetInputAttributes()) {
|
||||||
const Attribute::Index index = element.first;
|
|
||||||
if (!IsGenericAttribute(index)) {
|
if (!IsGenericAttribute(index)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
#include "video_core/engines/shader_bytecode.h"
|
#include "video_core/engines/shader_bytecode.h"
|
||||||
#include "video_core/shader/shader_ir.h"
|
#include "video_core/shader/shader_ir.h"
|
||||||
|
|
||||||
|
#pragma optimize("", off)
|
||||||
|
|
||||||
namespace VideoCommon::Shader {
|
namespace VideoCommon::Shader {
|
||||||
|
|
||||||
using Tegra::Shader::Attribute;
|
using Tegra::Shader::Attribute;
|
||||||
|
@ -47,17 +49,20 @@ u32 ShaderIR::DecodeMemory(NodeBlock& bb, u32 pc) {
|
||||||
"Indirect attribute loads are not supported");
|
"Indirect attribute loads are not supported");
|
||||||
UNIMPLEMENTED_IF_MSG((instr.attribute.fmt20.immediate.Value() % sizeof(u32)) != 0,
|
UNIMPLEMENTED_IF_MSG((instr.attribute.fmt20.immediate.Value() % sizeof(u32)) != 0,
|
||||||
"Unaligned attribute loads are not supported");
|
"Unaligned attribute loads are not supported");
|
||||||
|
UNIMPLEMENTED_IF_MSG(instr.attribute.fmt20.IsPhysical() &&
|
||||||
|
instr.attribute.fmt20.size != Tegra::Shader::AttributeSize::Word,
|
||||||
|
"Non-32 bits PHYS reads are not implemented");
|
||||||
|
|
||||||
Tegra::Shader::IpaMode input_mode{Tegra::Shader::IpaInterpMode::Pass,
|
const Node buffer{GetRegister(instr.gpr39)};
|
||||||
Tegra::Shader::IpaSampleMode::Default};
|
|
||||||
|
|
||||||
u64 next_element = instr.attribute.fmt20.element;
|
u64 next_element = instr.attribute.fmt20.element;
|
||||||
auto next_index = static_cast<u64>(instr.attribute.fmt20.index.Value());
|
auto next_index = static_cast<u64>(instr.attribute.fmt20.index.Value());
|
||||||
|
|
||||||
const auto LoadNextElement = [&](u32 reg_offset) {
|
const auto LoadNextElement = [&](u32 reg_offset) {
|
||||||
const Node buffer = GetRegister(instr.gpr39);
|
const Node attribute{instr.attribute.fmt20.IsPhysical()
|
||||||
const Node attribute = GetInputAttribute(static_cast<Attribute::Index>(next_index),
|
? GetPhysicalInputAttribute(instr.gpr8, buffer)
|
||||||
next_element, input_mode, buffer);
|
: GetInputAttribute(static_cast<Attribute::Index>(next_index),
|
||||||
|
next_element, buffer)};
|
||||||
|
|
||||||
SetRegister(bb, instr.gpr0.Value() + reg_offset, attribute);
|
SetRegister(bb, instr.gpr0.Value() + reg_offset, attribute);
|
||||||
|
|
||||||
|
@ -239,6 +244,21 @@ u32 ShaderIR::DecodeMemory(NodeBlock& bb, u32 pc) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OpCode::Id::AL2P: {
|
||||||
|
// Ignore al2p.direction since we don't care about it.
|
||||||
|
|
||||||
|
// Calculate emulation fake physical address.
|
||||||
|
const Node fixed_address{Immediate(static_cast<u32>(instr.al2p.address))};
|
||||||
|
const Node reg{GetRegister(instr.gpr8)};
|
||||||
|
const Node fake_address{Operation(OperationCode::IAdd, NO_PRECISE, reg, fixed_address)};
|
||||||
|
|
||||||
|
// Set the fake address to target register.
|
||||||
|
SetRegister(bb, instr.gpr0, fake_address);
|
||||||
|
|
||||||
|
// Signal the shader IR to declare all possible attributes and varyings
|
||||||
|
uses_physical_attributes = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
UNIMPLEMENTED_MSG("Unhandled memory instruction: {}", opcode->get().GetName());
|
UNIMPLEMENTED_MSG("Unhandled memory instruction: {}", opcode->get().GetName());
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,15 +130,18 @@ u32 ShaderIR::DecodeOther(NodeBlock& bb, u32 pc) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case OpCode::Id::IPA: {
|
case OpCode::Id::IPA: {
|
||||||
const auto& attribute = instr.attribute.fmt28;
|
const bool is_physical = instr.ipa.idx && instr.gpr8.Value() != 0xff;
|
||||||
|
|
||||||
|
const auto attribute = instr.attribute.fmt28;
|
||||||
const Tegra::Shader::IpaMode input_mode{instr.ipa.interp_mode.Value(),
|
const Tegra::Shader::IpaMode input_mode{instr.ipa.interp_mode.Value(),
|
||||||
instr.ipa.sample_mode.Value()};
|
instr.ipa.sample_mode.Value()};
|
||||||
|
|
||||||
const Node attr = GetInputAttribute(attribute.index, attribute.element, input_mode);
|
Node value = is_physical ? GetPhysicalInputAttribute(instr.gpr8)
|
||||||
Node value = attr;
|
: GetInputAttribute(attribute.index, attribute.element);
|
||||||
const Tegra::Shader::Attribute::Index index = attribute.index.Value();
|
const Tegra::Shader::Attribute::Index index = attribute.index.Value();
|
||||||
if (index >= Tegra::Shader::Attribute::Index::Attribute_0 &&
|
const bool is_generic = index >= Tegra::Shader::Attribute::Index::Attribute_0 &&
|
||||||
index <= Tegra::Shader::Attribute::Index::Attribute_31) {
|
index <= Tegra::Shader::Attribute::Index::Attribute_31;
|
||||||
|
if (is_generic || is_physical) {
|
||||||
// TODO(Blinkhawk): There are cases where a perspective attribute use PASS.
|
// TODO(Blinkhawk): There are cases where a perspective attribute use PASS.
|
||||||
// In theory by setting them as perspective, OpenGL does the perspective correction.
|
// In theory by setting them as perspective, OpenGL does the perspective correction.
|
||||||
// A way must figured to reverse the last step of it.
|
// A way must figured to reverse the last step of it.
|
||||||
|
|
|
@ -96,13 +96,14 @@ Node ShaderIR::GetPredicate(bool immediate) {
|
||||||
return GetPredicate(static_cast<u64>(immediate ? Pred::UnusedIndex : Pred::NeverExecute));
|
return GetPredicate(static_cast<u64>(immediate ? Pred::UnusedIndex : Pred::NeverExecute));
|
||||||
}
|
}
|
||||||
|
|
||||||
Node ShaderIR::GetInputAttribute(Attribute::Index index, u64 element,
|
Node ShaderIR::GetInputAttribute(Attribute::Index index, u64 element, Node buffer) {
|
||||||
const Tegra::Shader::IpaMode& input_mode, Node buffer) {
|
used_input_attributes.emplace(index);
|
||||||
const auto [entry, is_new] =
|
return StoreNode(AbufNode(index, static_cast<u32>(element), buffer));
|
||||||
used_input_attributes.emplace(std::make_pair(index, std::set<Tegra::Shader::IpaMode>{}));
|
}
|
||||||
entry->second.insert(input_mode);
|
|
||||||
|
|
||||||
return StoreNode(AbufNode(index, static_cast<u32>(element), input_mode, buffer));
|
Node ShaderIR::GetPhysicalInputAttribute(Tegra::Shader::Register physical_address, Node buffer) {
|
||||||
|
uses_physical_attributes = true;
|
||||||
|
return StoreNode(AbufNode(GetRegister(physical_address), buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
Node ShaderIR::GetOutputAttribute(Attribute::Index index, u64 element, Node buffer) {
|
Node ShaderIR::GetOutputAttribute(Attribute::Index index, u64 element, Node buffer) {
|
||||||
|
|
|
@ -456,17 +456,14 @@ private:
|
||||||
/// Attribute buffer memory (known as attributes or varyings in GLSL terms)
|
/// Attribute buffer memory (known as attributes or varyings in GLSL terms)
|
||||||
class AbufNode final {
|
class AbufNode final {
|
||||||
public:
|
public:
|
||||||
explicit constexpr AbufNode(Tegra::Shader::Attribute::Index index, u32 element,
|
// Initialize for standard attributes (index is explicit).
|
||||||
const Tegra::Shader::IpaMode& input_mode, Node buffer = {})
|
|
||||||
: input_mode{input_mode}, buffer{buffer}, index{index}, element{element} {}
|
|
||||||
|
|
||||||
explicit constexpr AbufNode(Tegra::Shader::Attribute::Index index, u32 element,
|
explicit constexpr AbufNode(Tegra::Shader::Attribute::Index index, u32 element,
|
||||||
Node buffer = {})
|
Node buffer = {})
|
||||||
: input_mode{}, buffer{buffer}, index{index}, element{element} {}
|
: buffer{buffer}, index{index}, element{element} {}
|
||||||
|
|
||||||
Tegra::Shader::IpaMode GetInputMode() const {
|
// Initialize for physical attributes (index is a variable value).
|
||||||
return input_mode;
|
explicit constexpr AbufNode(Node physical_address, Node buffer = {})
|
||||||
}
|
: physical_address{physical_address}, buffer{buffer} {}
|
||||||
|
|
||||||
Tegra::Shader::Attribute::Index GetIndex() const {
|
Tegra::Shader::Attribute::Index GetIndex() const {
|
||||||
return index;
|
return index;
|
||||||
|
@ -480,11 +477,19 @@ public:
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsPhysicalBuffer() const {
|
||||||
|
return physical_address != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node GetPhysicalAddress() const {
|
||||||
|
return physical_address;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Tegra::Shader::IpaMode input_mode;
|
Node physical_address{};
|
||||||
const Node buffer;
|
Node buffer{};
|
||||||
const Tegra::Shader::Attribute::Index index;
|
Tegra::Shader::Attribute::Index index{};
|
||||||
const u32 element;
|
u32 element{};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Constant buffer node, usually mapped to uniform buffers in GLSL
|
/// Constant buffer node, usually mapped to uniform buffers in GLSL
|
||||||
|
@ -573,8 +578,7 @@ public:
|
||||||
return used_predicates;
|
return used_predicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::map<Tegra::Shader::Attribute::Index, std::set<Tegra::Shader::IpaMode>>&
|
const std::set<Tegra::Shader::Attribute::Index>& GetInputAttributes() const {
|
||||||
GetInputAttributes() const {
|
|
||||||
return used_input_attributes;
|
return used_input_attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,6 +607,10 @@ public:
|
||||||
return static_cast<std::size_t>(coverage_end * sizeof(u64));
|
return static_cast<std::size_t>(coverage_end * sizeof(u64));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HasPhysicalAttributes() const {
|
||||||
|
return uses_physical_attributes;
|
||||||
|
}
|
||||||
|
|
||||||
const Tegra::Shader::Header& GetHeader() const {
|
const Tegra::Shader::Header& GetHeader() const {
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
@ -684,8 +692,9 @@ private:
|
||||||
/// Generates a predicate node for an immediate true or false value
|
/// Generates a predicate node for an immediate true or false value
|
||||||
Node GetPredicate(bool immediate);
|
Node GetPredicate(bool immediate);
|
||||||
/// Generates a node representing an input attribute. Keeps track of used attributes.
|
/// Generates a node representing an input attribute. Keeps track of used attributes.
|
||||||
Node GetInputAttribute(Tegra::Shader::Attribute::Index index, u64 element,
|
Node GetInputAttribute(Tegra::Shader::Attribute::Index index, u64 element, Node buffer = {});
|
||||||
const Tegra::Shader::IpaMode& input_mode, Node buffer = {});
|
/// Generates a node representing a physical input attribute.
|
||||||
|
Node GetPhysicalInputAttribute(Tegra::Shader::Register physical_address, Node buffer = {});
|
||||||
/// Generates a node representing an output attribute. Keeps track of used attributes.
|
/// Generates a node representing an output attribute. Keeps track of used attributes.
|
||||||
Node GetOutputAttribute(Tegra::Shader::Attribute::Index index, u64 element, Node buffer);
|
Node GetOutputAttribute(Tegra::Shader::Attribute::Index index, u64 element, Node buffer);
|
||||||
/// Generates a node representing an internal flag
|
/// Generates a node representing an internal flag
|
||||||
|
@ -859,13 +868,13 @@ private:
|
||||||
|
|
||||||
std::set<u32> used_registers;
|
std::set<u32> used_registers;
|
||||||
std::set<Tegra::Shader::Pred> used_predicates;
|
std::set<Tegra::Shader::Pred> used_predicates;
|
||||||
std::map<Tegra::Shader::Attribute::Index, std::set<Tegra::Shader::IpaMode>>
|
std::set<Tegra::Shader::Attribute::Index> used_input_attributes;
|
||||||
used_input_attributes;
|
|
||||||
std::set<Tegra::Shader::Attribute::Index> used_output_attributes;
|
std::set<Tegra::Shader::Attribute::Index> used_output_attributes;
|
||||||
std::map<u32, ConstBuffer> used_cbufs;
|
std::map<u32, ConstBuffer> used_cbufs;
|
||||||
std::set<Sampler> used_samplers;
|
std::set<Sampler> used_samplers;
|
||||||
std::array<bool, Tegra::Engines::Maxwell3D::Regs::NumClipDistances> used_clip_distances{};
|
std::array<bool, Tegra::Engines::Maxwell3D::Regs::NumClipDistances> used_clip_distances{};
|
||||||
std::map<GlobalMemoryBase, GlobalMemoryUsage> used_global_memory;
|
std::map<GlobalMemoryBase, GlobalMemoryUsage> used_global_memory;
|
||||||
|
bool uses_physical_attributes{}; // Shader uses AL2P or physical attribute read/writes
|
||||||
|
|
||||||
Tegra::Shader::Header header;
|
Tegra::Shader::Header header;
|
||||||
};
|
};
|
||||||
|
|
Reference in New Issue