[GPU] Shader translator refactoring (mostly ALU), fixes for disassembly round trip and write masks
This commit is contained in:
@@ -10,10 +10,12 @@
|
||||
#ifndef XENIA_GPU_SHADER_H_
|
||||
#define XENIA_GPU_SHADER_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/math.h"
|
||||
#include "xenia/base/string_buffer.h"
|
||||
#include "xenia/gpu/ucode.h"
|
||||
#include "xenia/gpu/xenos.h"
|
||||
@@ -21,23 +23,32 @@
|
||||
namespace xe {
|
||||
namespace gpu {
|
||||
|
||||
// The structures here are used for both translation and disassembly.
|
||||
//
|
||||
// Because disassembly uses them too, to make sure "assemble -> disassemble ->
|
||||
// reassemble" round trip is always successful with the XNA assembler (as it is
|
||||
// the accuracy benchmark for translation), only generalization - not
|
||||
// optimization like nop skipping/replacement - must be done while converting
|
||||
// microcode to these structures (in other words, parsed shader code should be
|
||||
// enough to accurately reconstruct the microcode for any shader that could be
|
||||
// written by a human in assembly).
|
||||
//
|
||||
// During the "parsed -> host" part of the translation, however, translators are
|
||||
// free to make any optimizations (as long as they don't affect the result, of
|
||||
// course) they find appropriate.
|
||||
|
||||
enum class InstructionStorageTarget {
|
||||
// Result is not stored.
|
||||
kNone,
|
||||
// Result is stored to a temporary register indexed by storage_index [0-31].
|
||||
kRegister,
|
||||
// Result is stored into a vertex shader interpolant export [0-15].
|
||||
kInterpolant,
|
||||
// Result is stored into a vertex shader interpolator export [0-15].
|
||||
kInterpolator,
|
||||
// Result is stored to the position export (gl_Position).
|
||||
kPosition,
|
||||
// Result is stored to the vertex shader misc export register.
|
||||
// See R6xx/R7xx registers for details (USE_VTX_POINT_SIZE, USE_VTX_EDGE_FLAG,
|
||||
// USE_VTX_KILL_FLAG).
|
||||
// X - PSIZE (gl_PointSize).
|
||||
// Y - EDGEFLAG (glEdgeFlag) for PrimitiveType::kPolygon wireframe/point
|
||||
// drawing.
|
||||
// Z - KILLVERTEX flag (used in Banjo-Kazooie: Nuts & Bolts for grass), set
|
||||
// for killing primitives based on PA_CL_CLIP_CNTL::VTX_KILL_OR condition.
|
||||
// Result is stored to the vertex shader misc export register, see
|
||||
// ucode::ExportRegister::kVSPointSizeEdgeFlagKillVertex for description of
|
||||
// components.
|
||||
kPointSizeEdgeFlagKillVertex,
|
||||
// Result is stored as memexport destination address
|
||||
// (see xenos::xe_gpu_memexport_stream_t).
|
||||
@@ -45,11 +56,29 @@ enum class InstructionStorageTarget {
|
||||
// Result is stored to memexport destination data.
|
||||
kExportData,
|
||||
// Result is stored to a color target export indexed by storage_index [0-3].
|
||||
kColorTarget,
|
||||
// Result is stored to the depth export (gl_FragDepth).
|
||||
kColor,
|
||||
// X of the result is stored to the depth export (gl_FragDepth).
|
||||
kDepth,
|
||||
};
|
||||
|
||||
// Must be used only in translation to skip unused components, but not in
|
||||
// disassembly (because oPts.x000 will be assembled, but oPts.x00_ has both
|
||||
// skipped components and zeros, which cannot be encoded, and therefore it will
|
||||
// not).
|
||||
constexpr uint32_t GetInstructionStorageTargetUsedComponents(
|
||||
InstructionStorageTarget target) {
|
||||
switch (target) {
|
||||
case InstructionStorageTarget::kNone:
|
||||
return 0b0000;
|
||||
case InstructionStorageTarget::kPointSizeEdgeFlagKillVertex:
|
||||
return 0b0111;
|
||||
case InstructionStorageTarget::kDepth:
|
||||
return 0b0001;
|
||||
default:
|
||||
return 0b1111;
|
||||
}
|
||||
}
|
||||
|
||||
enum class InstructionStorageAddressingMode {
|
||||
// The storage index is not dynamically addressed.
|
||||
kStatic,
|
||||
@@ -75,71 +104,63 @@ enum class SwizzleSource {
|
||||
k1,
|
||||
};
|
||||
|
||||
constexpr SwizzleSource GetSwizzleFromComponentIndex(int i) {
|
||||
constexpr SwizzleSource GetSwizzleFromComponentIndex(uint32_t i) {
|
||||
return static_cast<SwizzleSource>(i);
|
||||
}
|
||||
inline char GetCharForComponentIndex(int i) {
|
||||
inline char GetCharForComponentIndex(uint32_t i) {
|
||||
const static char kChars[] = {'x', 'y', 'z', 'w'};
|
||||
return kChars[i];
|
||||
}
|
||||
inline char GetCharForSwizzle(SwizzleSource swizzle_source) {
|
||||
const static char kChars[] = {'x', 'y', 'z', 'w', '0', '1'};
|
||||
return kChars[static_cast<int>(swizzle_source)];
|
||||
return kChars[static_cast<uint32_t>(swizzle_source)];
|
||||
}
|
||||
|
||||
struct InstructionResult {
|
||||
// Where the result is going.
|
||||
InstructionStorageTarget storage_target = InstructionStorageTarget::kNone;
|
||||
// Index into the storage_target, if it is indexed.
|
||||
int storage_index = 0;
|
||||
uint32_t storage_index = 0;
|
||||
// How the storage index is dynamically addressed, if it is.
|
||||
InstructionStorageAddressingMode storage_addressing_mode =
|
||||
InstructionStorageAddressingMode::kStatic;
|
||||
// True if the result is exporting from the shader.
|
||||
bool is_export = false;
|
||||
// True to clamp the result value to [0-1].
|
||||
bool is_clamped = false;
|
||||
// Defines whether each output component is written.
|
||||
bool write_mask[4] = {false, false, false, false};
|
||||
// Defines whether each output component is written, though this is from the
|
||||
// original microcode, not taking into account whether such components
|
||||
// actually exist in the target.
|
||||
uint32_t original_write_mask = 0b0000;
|
||||
// Defines the source for each output component xyzw.
|
||||
SwizzleSource components[4] = {SwizzleSource::kX, SwizzleSource::kY,
|
||||
SwizzleSource::kZ, SwizzleSource::kW};
|
||||
// Returns true if any component is written to.
|
||||
bool has_any_writes() const {
|
||||
return write_mask[0] || write_mask[1] || write_mask[2] || write_mask[3];
|
||||
}
|
||||
// Returns true if all components are written to.
|
||||
bool has_all_writes() const {
|
||||
return write_mask[0] && write_mask[1] && write_mask[2] && write_mask[3];
|
||||
}
|
||||
// Returns number of components written
|
||||
uint32_t num_writes() const {
|
||||
uint32_t total = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (write_mask[i]) {
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
// Returns true if any non-constant components are written.
|
||||
bool stores_non_constants() const {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (write_mask[i] && components[i] != SwizzleSource::k0 &&
|
||||
components[i] != SwizzleSource::k1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
// Returns the write mask containing only components actually present in the
|
||||
// target.
|
||||
uint32_t GetUsedWriteMask() const {
|
||||
return original_write_mask &
|
||||
GetInstructionStorageTargetUsedComponents(storage_target);
|
||||
}
|
||||
// True if the components are in their 'standard' swizzle arrangement (xyzw).
|
||||
bool is_standard_swizzle() const {
|
||||
return has_all_writes() && components[0] == SwizzleSource::kX &&
|
||||
bool IsStandardSwizzle() const {
|
||||
return (GetUsedWriteMask() == 0b1111) &&
|
||||
components[0] == SwizzleSource::kX &&
|
||||
components[1] == SwizzleSource::kY &&
|
||||
components[2] == SwizzleSource::kZ &&
|
||||
components[3] == SwizzleSource::kW;
|
||||
}
|
||||
// Returns the components of the result, before swizzling, that won't be
|
||||
// discarded or replaced with a constant.
|
||||
uint32_t GetUsedResultComponents() const {
|
||||
uint32_t used_write_mask = GetUsedWriteMask();
|
||||
uint32_t used_components = 0b0000;
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
if ((used_write_mask & (1 << i)) && components[i] >= SwizzleSource::kX &&
|
||||
components[i] <= SwizzleSource::kW) {
|
||||
used_components |=
|
||||
1 << (uint32_t(components[i]) - uint32_t(SwizzleSource::kX));
|
||||
}
|
||||
}
|
||||
return used_components;
|
||||
}
|
||||
};
|
||||
|
||||
enum class InstructionStorageSource {
|
||||
@@ -159,7 +180,7 @@ struct InstructionOperand {
|
||||
// Where the source comes from.
|
||||
InstructionStorageSource storage_source = InstructionStorageSource::kRegister;
|
||||
// Index into the storage_target, if it is indexed.
|
||||
int storage_index = 0;
|
||||
uint32_t storage_index = 0;
|
||||
// How the storage index is dynamically addressed, if it is.
|
||||
InstructionStorageAddressingMode storage_addressing_mode =
|
||||
InstructionStorageAddressingMode::kStatic;
|
||||
@@ -168,13 +189,19 @@ struct InstructionOperand {
|
||||
// True to take the absolute value of the source (before any negation).
|
||||
bool is_absolute_value = false;
|
||||
// Number of components taken from the source operand.
|
||||
int component_count = 0;
|
||||
uint32_t component_count = 4;
|
||||
// Defines the source for each component xyzw (up to the given
|
||||
// component_count).
|
||||
SwizzleSource components[4] = {SwizzleSource::kX, SwizzleSource::kY,
|
||||
SwizzleSource::kZ, SwizzleSource::kW};
|
||||
// Returns the swizzle source for the component, replicating the rightmost
|
||||
// component if there are less than 4 components (similar to what the Xbox 360
|
||||
// shader compiler does as a general rule for unspecified components).
|
||||
SwizzleSource GetComponent(uint32_t index) const {
|
||||
return components[std::min(index, component_count - 1)];
|
||||
}
|
||||
// True if the components are in their 'standard' swizzle arrangement (xyzw).
|
||||
bool is_standard_swizzle() const {
|
||||
bool IsStandardSwizzle() const {
|
||||
switch (component_count) {
|
||||
case 4:
|
||||
return components[0] == SwizzleSource::kX &&
|
||||
@@ -185,26 +212,32 @@ struct InstructionOperand {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether absolute values of two operands are identical (useful for emulating
|
||||
// Shader Model 3 0*anything=0 multiplication behavior).
|
||||
bool EqualsAbsolute(const InstructionOperand& other) const {
|
||||
// Returns which components of two operands are identical, but may have
|
||||
// different signs (for simplicity of usage with GetComponent, treating the
|
||||
// rightmost component as replicated).
|
||||
uint32_t GetAbsoluteIdenticalComponents(
|
||||
const InstructionOperand& other) const {
|
||||
if (storage_source != other.storage_source ||
|
||||
storage_index != other.storage_index ||
|
||||
storage_addressing_mode != other.storage_addressing_mode ||
|
||||
component_count != other.component_count) {
|
||||
return false;
|
||||
storage_addressing_mode != other.storage_addressing_mode) {
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < component_count; ++i) {
|
||||
if (components[i] != other.components[i]) {
|
||||
return false;
|
||||
}
|
||||
uint32_t identical_components = 0;
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
identical_components |= uint32_t(GetComponent(i) == other.GetComponent(i))
|
||||
<< i;
|
||||
}
|
||||
return true;
|
||||
return identical_components;
|
||||
}
|
||||
|
||||
bool operator==(const InstructionOperand& other) const {
|
||||
return EqualsAbsolute(other) && is_negated == other.is_negated &&
|
||||
is_absolute_value == other.is_absolute_value;
|
||||
// Returns which components of two operands will always be bitwise equal, but
|
||||
// may have different signs (disregarding component_count for simplicity of
|
||||
// usage with GetComponent, treating the rightmost component as replicated).
|
||||
uint32_t GetIdenticalComponents(const InstructionOperand& other) const {
|
||||
if (is_negated != other.is_negated ||
|
||||
is_absolute_value != other.is_absolute_value) {
|
||||
return 0;
|
||||
}
|
||||
return GetAbsoluteIdenticalComponents(other);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -365,9 +398,6 @@ struct ParsedAllocInstruction {
|
||||
};
|
||||
|
||||
struct ParsedVertexFetchInstruction {
|
||||
// Index into the ucode dword source.
|
||||
uint32_t dword_index = 0;
|
||||
|
||||
// Opcode for the instruction.
|
||||
ucode::FetchOpcode opcode;
|
||||
// Friendly name of the instruction.
|
||||
@@ -409,9 +439,6 @@ struct ParsedVertexFetchInstruction {
|
||||
};
|
||||
|
||||
struct ParsedTextureFetchInstruction {
|
||||
// Index into the ucode dword source.
|
||||
uint32_t dword_index = 0;
|
||||
|
||||
// Opcode for the instruction.
|
||||
ucode::FetchOpcode opcode;
|
||||
// Friendly name of the instruction.
|
||||
@@ -462,17 +489,6 @@ struct ParsedTextureFetchInstruction {
|
||||
};
|
||||
|
||||
struct ParsedAluInstruction {
|
||||
// Index into the ucode dword source.
|
||||
uint32_t dword_index = 0;
|
||||
|
||||
// True if the vector part of the instruction needs to be executed and data
|
||||
// about it in this structure is valid.
|
||||
bool has_vector_op = false;
|
||||
// True if the scalar part of the instruction needs to be executed and data
|
||||
// about it in this structure is valid.
|
||||
bool has_scalar_op = false;
|
||||
bool is_nop() const { return !has_vector_op && !has_scalar_op; }
|
||||
|
||||
// Opcode for the vector part of the instruction.
|
||||
ucode::AluVectorOpcode vector_opcode = ucode::AluVectorOpcode::kAdd;
|
||||
// Opcode for the scalar part of the instruction.
|
||||
@@ -488,8 +504,20 @@ struct ParsedAluInstruction {
|
||||
// Expected predication condition value if predicated.
|
||||
bool predicate_condition = false;
|
||||
|
||||
// Describes how the vector operation result is stored.
|
||||
InstructionResult vector_result;
|
||||
// Describes how the vector operation result and, for exports, constant 0/1
|
||||
// are stored. For simplicity of translation and disassembly, treating
|
||||
// constant 0/1 writes as a part of the vector operation - they need to be
|
||||
// expressed somehow in the disassembly anyway with a properly disassembled
|
||||
// instruction even if only constants are being exported. The XNA disassembler
|
||||
// falls back to displaying the whole vector operation, even if only constant
|
||||
// components are written, if the scalar operation is a nop or if the vector
|
||||
// operation has side effects (but if the scalar operation isn't nop, it
|
||||
// outputs the entire constant mask in the scalar operation destination).
|
||||
// Normally the XNA disassembler outputs the constant mask in both vector and
|
||||
// scalar operations, but that's not required by assembler, so it doesn't
|
||||
// really matter whether it's specified in the vector operation, in the scalar
|
||||
// operation, or in both.
|
||||
InstructionResult vector_and_constant_result;
|
||||
// Describes how the scalar operation result is stored.
|
||||
InstructionResult scalar_result;
|
||||
// Both operations must be executed before any result is stored if vector and
|
||||
@@ -499,27 +527,109 @@ struct ParsedAluInstruction {
|
||||
// operations.
|
||||
|
||||
// Number of source operands of the vector operation.
|
||||
size_t vector_operand_count = 0;
|
||||
uint32_t vector_operand_count = 0;
|
||||
// Describes each source operand of the vector operation.
|
||||
InstructionOperand vector_operands[3];
|
||||
// Number of source operands of the scalar operation.
|
||||
size_t scalar_operand_count = 0;
|
||||
uint32_t scalar_operand_count = 0;
|
||||
// Describes each source operand of the scalar operation.
|
||||
InstructionOperand scalar_operands[2];
|
||||
|
||||
// If this is a valid eA write (MAD with a stream constant), returns the index
|
||||
// of the stream float constant, otherwise returns UINT32_MAX.
|
||||
// Whether the vector part of the instruction is the same as if it was omitted
|
||||
// in the assembly (if compiled or assembled with the Xbox 360 shader
|
||||
// compiler), and thus reassembling the shader with this instruction omitted
|
||||
// will result in the same microcode (since instructions with just an empty
|
||||
// write mask may have different values in other fields).
|
||||
// This is for disassembly! Translators should use the write masks and
|
||||
// AluVectorOpHasSideEffects to skip operations, as this only covers one very
|
||||
// specific nop format!
|
||||
bool IsVectorOpDefaultNop() const {
|
||||
if (vector_opcode != ucode::AluVectorOpcode::kMax ||
|
||||
vector_and_constant_result.original_write_mask ||
|
||||
vector_and_constant_result.is_clamped ||
|
||||
vector_operands[0].storage_source !=
|
||||
InstructionStorageSource::kRegister ||
|
||||
vector_operands[0].storage_index != 0 ||
|
||||
vector_operands[0].storage_addressing_mode !=
|
||||
InstructionStorageAddressingMode::kStatic ||
|
||||
vector_operands[0].is_negated || vector_operands[0].is_absolute_value ||
|
||||
!vector_operands[0].IsStandardSwizzle() ||
|
||||
vector_operands[1].storage_source !=
|
||||
InstructionStorageSource::kRegister ||
|
||||
vector_operands[1].storage_index != 0 ||
|
||||
vector_operands[1].storage_addressing_mode !=
|
||||
InstructionStorageAddressingMode::kStatic ||
|
||||
vector_operands[1].is_negated || vector_operands[1].is_absolute_value ||
|
||||
!vector_operands[1].IsStandardSwizzle()) {
|
||||
return false;
|
||||
}
|
||||
if (vector_and_constant_result.storage_target ==
|
||||
InstructionStorageTarget::kRegister) {
|
||||
if (vector_and_constant_result.storage_index != 0 ||
|
||||
vector_and_constant_result.storage_addressing_mode !=
|
||||
InstructionStorageAddressingMode::kStatic) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// In case both vector and scalar operations are nop, still need to write
|
||||
// somewhere that it's an export, not mov r0._, r0 + retain_prev r0._.
|
||||
// Accurate round trip is possible only if the target is o0 or oC0,
|
||||
// because if the total write mask is empty, the XNA assembler forces the
|
||||
// destination to be o0/oC0, but this doesn't really matter in this case.
|
||||
if (IsScalarOpDefaultNop()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whether the scalar part of the instruction is the same as if it was omitted
|
||||
// in the assembly (if compiled or assembled with the Xbox 360 shader
|
||||
// compiler), and thus reassembling the shader with this instruction omitted
|
||||
// will result in the same microcode (since instructions with just an empty
|
||||
// write mask may have different values in other fields).
|
||||
bool IsScalarOpDefaultNop() const {
|
||||
if (scalar_opcode != ucode::AluScalarOpcode::kRetainPrev ||
|
||||
scalar_result.original_write_mask || scalar_result.is_clamped) {
|
||||
return false;
|
||||
}
|
||||
if (scalar_result.storage_target == InstructionStorageTarget::kRegister) {
|
||||
if (scalar_result.storage_index != 0 ||
|
||||
scalar_result.storage_addressing_mode !=
|
||||
InstructionStorageAddressingMode::kStatic) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// For exports, if both are nop, the vector operation will be kept to state
|
||||
// in the microcode that the destination in the microcode is an export.
|
||||
return true;
|
||||
}
|
||||
|
||||
// For translation (not disassembly) - whether this instruction has totally no
|
||||
// effect.
|
||||
bool IsNop() const {
|
||||
return scalar_opcode == ucode::AluScalarOpcode::kRetainPrev &&
|
||||
!scalar_result.GetUsedWriteMask() &&
|
||||
!vector_and_constant_result.GetUsedWriteMask() &&
|
||||
!ucode::AluVectorOpHasSideEffects(vector_opcode);
|
||||
}
|
||||
|
||||
// If this is a "normal" eA write recognized by Xenia (MAD with a stream
|
||||
// constant), returns the index of the stream float constant, otherwise
|
||||
// returns UINT32_MAX.
|
||||
uint32_t GetMemExportStreamConstant() const {
|
||||
if (has_vector_op &&
|
||||
vector_result.storage_target ==
|
||||
if (vector_and_constant_result.storage_target ==
|
||||
InstructionStorageTarget::kExportAddress &&
|
||||
vector_opcode == ucode::AluVectorOpcode::kMad &&
|
||||
vector_result.has_all_writes() &&
|
||||
vector_and_constant_result.GetUsedResultComponents() == 0b1111 &&
|
||||
!vector_and_constant_result.is_clamped &&
|
||||
vector_operands[2].storage_source ==
|
||||
InstructionStorageSource::kConstantFloat &&
|
||||
vector_operands[2].storage_addressing_mode ==
|
||||
InstructionStorageAddressingMode::kStatic &&
|
||||
vector_operands[2].is_standard_swizzle()) {
|
||||
vector_operands[2].IsStandardSwizzle() &&
|
||||
!vector_operands[2].is_negated &&
|
||||
!vector_operands[2].is_absolute_value) {
|
||||
return vector_operands[2].storage_index;
|
||||
}
|
||||
return UINT32_MAX;
|
||||
@@ -581,9 +691,8 @@ class Shader {
|
||||
struct ConstantRegisterMap {
|
||||
// Bitmap of all kConstantFloat registers read by the shader.
|
||||
// Any shader can only read up to 256 of the 512, and the base is dependent
|
||||
// on the shader type. Each bit corresponds to a storage index from the type
|
||||
// base, so bit 0 in a vertex shader is register 0, and bit 0 in a fragment
|
||||
// shader is register 256.
|
||||
// on the shader type and SQ_VS/PS_CONST registers. Each bit corresponds to
|
||||
// a storage index from the type base.
|
||||
uint64_t float_bitmap[256 / 64];
|
||||
// Bitmap of all loop constants read by the shader.
|
||||
// Each bit corresponds to a storage index [0-31].
|
||||
@@ -595,8 +704,33 @@ class Shader {
|
||||
// Total number of kConstantFloat registers read by the shader.
|
||||
uint32_t float_count;
|
||||
|
||||
// Computed byte count of all registers required when packed.
|
||||
uint32_t packed_byte_length;
|
||||
// Whether kConstantFloat registers are indexed dynamically - in this case,
|
||||
// float_bitmap must be set to all 1, and tight packing must not be done.
|
||||
bool float_dynamic_addressing;
|
||||
|
||||
// Returns the index of the float4 constant as if all float4 constant
|
||||
// registers actually referenced were tightly packed in a buffer, or
|
||||
// UINT32_MAX if not found.
|
||||
uint32_t GetPackedFloatConstantIndex(uint32_t float_constant) const {
|
||||
if (float_constant >= 256) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
if (float_dynamic_addressing) {
|
||||
// Any can potentially be read - not packing.
|
||||
return float_constant;
|
||||
}
|
||||
uint32_t block_index = float_constant / 64;
|
||||
uint32_t bit_index = float_constant % 64;
|
||||
if (!(float_bitmap[block_index] & (uint64_t(1) << bit_index))) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
uint32_t offset = 0;
|
||||
for (uint32_t i = 0; i < block_index; ++i) {
|
||||
offset += xe::bit_count(float_bitmap[i]);
|
||||
}
|
||||
return offset + xe::bit_count(float_bitmap[block_index] &
|
||||
((uint64_t(1) << bit_index) - 1));
|
||||
}
|
||||
};
|
||||
|
||||
Shader(ShaderType shader_type, uint64_t ucode_data_hash,
|
||||
@@ -642,7 +776,9 @@ class Shader {
|
||||
}
|
||||
|
||||
// Returns true if the given color target index [0-3].
|
||||
bool writes_color_target(int i) const { return writes_color_targets_[i]; }
|
||||
bool writes_color_target(uint32_t i) const {
|
||||
return writes_color_targets_[i];
|
||||
}
|
||||
|
||||
// True if the shader overrides the pixel depth.
|
||||
bool writes_depth() const { return writes_depth_; }
|
||||
|
||||
Reference in New Issue
Block a user