Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[d3d9] Implement several IDirect3DShaderValidator9 validations #4398

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 220 additions & 15 deletions src/d3d9/d3d9_shader_validator.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,56 @@

#include "d3d9_include.h"

#include "../dxso/dxso_header.h"
#include "../dxso/dxso_decoder.h"

namespace dxvk {

enum class D3D9ShaderValidatorMessage : uint32_t {
BeginOutOfOrder = 0xeb,
InstructionOutOfOrder = 0xec,
InstructionEndOfShader = 0xed,
InstructionNullArgs = 0xee,
BadVersionTokenLength = 0xef,
BadVersionTokenType = 0xf0,
BadEndToken = 0xf1,
EndOutOfOrder = 0xf2,
MissingEndToken = 0xf3,
BadInputRegisterDeclaration = 0x12c,
BadInputRegister = 0x167,
BadInstructionLength = 0x21e,
};

enum class D3D9ShaderValidatorState {
Begin,
ValidatingHeader,
ValidatingInstructions,
EndOfShader,
Error,
};

using D3D9ShaderValidatorCallback = HRESULT(STDMETHODCALLTYPE *)(
const char* pFile,
UINT Line,
DWORD Unknown,
D3D9ShaderValidatorMessage MessageID,
const char* pMessage,
void* pUserData);

class IDirect3DShaderValidator9 : public IUnknown {

public:

virtual HRESULT STDMETHODCALLTYPE Begin(
void* pCallback,
void* pUserParam,
DWORD Unknown) = 0;
D3D9ShaderValidatorCallback pCallback,
void* pUserParam,
DWORD Unknown) = 0;

virtual HRESULT STDMETHODCALLTYPE Instruction(
const char* pUnknown1,
UINT Unknown2,
const DWORD* pInstruction,
DWORD InstructionLength) = 0;
const DWORD* pdwInst,
DWORD cdw) = 0;

virtual HRESULT STDMETHODCALLTYPE End() = 0;

Expand All @@ -37,32 +71,203 @@ namespace dxvk {


HRESULT STDMETHODCALLTYPE Begin(
void* pCallback,
void* pUserParam,
DWORD Unknown) {
Logger::debug("D3D9ShaderValidator::Begin: Stub");
D3D9ShaderValidatorCallback pCallback,
void* pUserData,
DWORD Unknown) {
if (unlikely(m_state != D3D9ShaderValidatorState::Begin)) {
return ErrorCallback(nullptr, -1, 0,
D3D9ShaderValidatorMessage::BeginOutOfOrder,
"IDirect3DShaderValidator9::Begin called out of order. ::End must be called first.");
}

m_callback = pCallback;
m_userData = pUserData;
m_state = D3D9ShaderValidatorState::ValidatingHeader;

return D3D_OK;
}


HRESULT STDMETHODCALLTYPE Instruction(
const char* pUnknown1,
UINT Unknown2,
const DWORD* pInstruction,
DWORD InstructionLength) {
Logger::debug("D3D9ShaderValidator::Instruction: Stub");
const char* pFile,
UINT Line,
const DWORD* pdwInst,
DWORD cdw) {
if (unlikely(pdwInst == nullptr || !cdw)) {
return ErrorCallback(pFile, Line, 0,
D3D9ShaderValidatorMessage::InstructionNullArgs,
"IDirect3DShaderValidator9::Instruction called with NULL == pdwInst or 0 == cdw.");
}

if (unlikely(m_state == D3D9ShaderValidatorState::Begin)) {
return ErrorCallback(pFile, Line, 0,
D3D9ShaderValidatorMessage::InstructionOutOfOrder,
"IDirect3DShaderValidator9::Instruction called out of order. ::Begin must be called first.");
} else if (unlikely(m_state == D3D9ShaderValidatorState::EndOfShader)) {
return ErrorCallback(pFile, Line, 0,
D3D9ShaderValidatorMessage::InstructionEndOfShader,
"IDirect3DShaderValidator9::Instruction called out of order. After end token there should be no more instructions. Call ::End next.");
} else if (unlikely(m_state == D3D9ShaderValidatorState::Error)) {
return E_FAIL;
}

if (m_state == D3D9ShaderValidatorState::ValidatingHeader)
return ValidateHeader(pFile, Line, pdwInst, cdw);

DxsoCodeIter pdwInstIter{ reinterpret_cast<const uint32_t*>(pdwInst) };
bool isEndToken = !m_ctx->decodeInstruction(pdwInstIter);
const DxsoInstructionContext instContext = m_ctx->getInstructionContext();

if (isEndToken)
return ValidateEndToken(pFile, Line, pdwInst, cdw);

// TODO: DxsoDecodeContext::decodeInstructionLength() does not currently appear
// to return the correct token length in many cases, and as such dwordLength
// will not be equal to cdw in many situations that are expected to pass validation
//
/*Logger::debug(str::format("IDirect3DShaderValidator9::Instruction: opcode ", instContext.instruction.opcode));
// + 1 to account for the opcode...
uint32_t dwordLength = instContext.instruction.tokenLength + 1;
Logger::debug(str::format("IDirect3DShaderValidator9::Instruction: cdw ", cdw));
Logger::debug(str::format("IDirect3DShaderValidator9::Instruction: dwordLength ", dwordLength));
if (unlikely(cdw != dwordLength)) {
return ErrorCallback(pFile, Line, 0x2,
D3D9ShaderValidatorMessage::BadInstructionLength,
str::format("Instruction length specified for instruction (", cdw, ") does not match the token count encountered (", dwordLength, "). Aborting validation."));
}*/

// a maximum of 10 inputs are supported with PS 3.0 (validation required by The Void)
if (m_isPixelShader && m_majorVersion == 3) {
switch (instContext.instruction.opcode) {
case DxsoOpcode::Comment:
case DxsoOpcode::Def:
case DxsoOpcode::DefB:
case DxsoOpcode::DefI:
break;

default:
for (uint32_t instNum = 1; pdwInst[instNum] & 0x80000000; instNum++) {
DWORD regType = ((pdwInst[instNum] & D3DSP_REGTYPE_MASK) >> D3DSP_REGTYPE_SHIFT)
| ((pdwInst[instNum] & D3DSP_REGTYPE_MASK2) >> D3DSP_REGTYPE_SHIFT2);
DWORD regIndex = pdwInst[instNum] & D3DSP_REGNUM_MASK;

if (unlikely(regType == D3DSPR_INPUT && regIndex >= 10)) {
return ErrorCallback(pFile, Line, 0x2,
instContext.instruction.opcode == DxsoOpcode::Dcl ?
D3D9ShaderValidatorMessage::BadInputRegisterDeclaration :
D3D9ShaderValidatorMessage::BadInputRegister,
"IDirect3DShaderValidator9::Instruction: Invalid number of PS input registers specified. Aborting validation.");
}
}
}
}

return D3D_OK;
}


HRESULT STDMETHODCALLTYPE End() {
Logger::debug("D3D9ShaderValidator::End: Stub");
if (unlikely(m_state == D3D9ShaderValidatorState::Error)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean the validator instance is not reusable if validation failed? Does this match native behaviour?

Copy link
Contributor Author

@WinterSnowfall WinterSnowfall Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good question for @misyltoad . I'm afraid I have no idea how to go about testing this. Some games do seem to recreate the validator instance for each shader from what I've noticed, but I have no idea if this is intended by design.

Copy link
Contributor Author

@WinterSnowfall WinterSnowfall Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S.: According to these tests it does indeed seem that once a validation error is hit, the validator instance must be released and recreated. Great API, I know.

return E_FAIL;
} else if (unlikely(m_state == D3D9ShaderValidatorState::Begin)) {
return ErrorCallback(nullptr, 0, 0,
D3D9ShaderValidatorMessage::EndOutOfOrder,
"IDirect3DShaderValidator9::End called out of order. Call to ::Begin, followed by calls to ::Instruction must occur first.");
} else if (unlikely(m_state != D3D9ShaderValidatorState::EndOfShader)) {
return ErrorCallback(nullptr, 0, 0,
D3D9ShaderValidatorMessage::MissingEndToken,
"IDirect3DShaderValidator9::End: Shader missing end token.");
}

m_state = D3D9ShaderValidatorState::Begin;
m_isPixelShader = false;
m_majorVersion = 0;
m_minorVersion = 0;
m_callback = nullptr;
m_userData = nullptr;
m_ctx = nullptr;

return D3D_OK;
}

private:

HRESULT ValidateHeader(const char* pFile, UINT Line, const DWORD* pdwInst, DWORD cdw) {
if (unlikely(cdw != 1)) {
return ErrorCallback(pFile, Line, 0x6,
D3D9ShaderValidatorMessage::BadVersionTokenLength,
"IDirect3DShaderValidator9::Instruction: Bad version token. DWORD count > 1 given. Expected DWORD count to be 1 for version token.");
}

DxsoReader reader = { reinterpret_cast<const char*>(pdwInst) };
uint32_t headerToken = reader.readu32();
uint32_t shaderType = headerToken & 0xffff0000;
DxsoProgramType programType;

if (shaderType == 0xffff0000) { // Pixel Shader
programType = DxsoProgramTypes::PixelShader;
m_isPixelShader = true;
} else if (shaderType == 0xfffe0000) { // Vertex Shader
programType = DxsoProgramTypes::VertexShader;
m_isPixelShader = false;
} else {
return ErrorCallback(pFile, Line, 0x6,
D3D9ShaderValidatorMessage::BadVersionTokenType,
"IDirect3DShaderValidator9::Instruction: Bad version token. It indicates neither a pixel shader nor a vertex shader.");
}

m_majorVersion = (headerToken >> 8) & 0xff;
m_minorVersion = headerToken & 0xff;
m_ctx = std::make_unique<DxsoDecodeContext>(DxsoProgramInfo{ programType, m_minorVersion, m_majorVersion });
m_state = D3D9ShaderValidatorState::ValidatingInstructions;

const char* shaderTypeOutput = m_isPixelShader ? "PS" : "VS";
Logger::debug(str::format("IDirect3DShaderValidator9::Instruction: Validating ",
shaderTypeOutput, " version ", m_majorVersion, ".", m_minorVersion));

return D3D_OK;
}

HRESULT ValidateEndToken(const char* pFile, UINT Line, const DWORD* pdwInst, DWORD cdw) {
// Reached the end token.
if (unlikely(cdw != 1)) {
return ErrorCallback(pFile, Line, 0x6,
D3D9ShaderValidatorMessage::BadEndToken,
"IDirect3DShaderValidator9::Instruction: Bad end token. DWORD count > 1 given. Expected DWORD count to be 1 for end token.");
}

m_state = D3D9ShaderValidatorState::EndOfShader;

return D3D_OK;
}

HRESULT ErrorCallback(
const char* pFile,
UINT Line,
DWORD Unknown,
D3D9ShaderValidatorMessage MessageID,
const std::string& Message) {
if (m_callback)
m_callback(pFile, Line, Unknown, MessageID, Message.c_str(), m_userData);

// TODO: Consider switching this to debug, once we're
// confident the implementation doesn't cause any issues
Logger::warn(Message);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we expect this to get called in every game that uses the validation functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only in case of actual validation errors, however some of these (like in the case of The Void) are expected behavior. It would be useful to see them in logs however, at least for a while, until we're sure it's not causing unintended validations in other places.

Testing so far has been promising, but you never know, since this is like entirely undocumented.


m_state = D3D9ShaderValidatorState::Error;

return E_FAIL;
}

bool m_isPixelShader = false;
uint32_t m_majorVersion = 0;
uint32_t m_minorVersion = 0;

D3D9ShaderValidatorState m_state = D3D9ShaderValidatorState::Begin;
D3D9ShaderValidatorCallback m_callback = nullptr;
void* m_userData = nullptr;

std::unique_ptr<DxsoDecodeContext> m_ctx;
};

}