Skip to content

Commit

Permalink
Add inline-assembly support
Browse files Browse the repository at this point in the history
So far, it is complicated and time-consuming to test the behavior of
certain instructions or their interactions. The easiest method to test
a sequence of instructions to date, is to make a simple graphics test,
disassemble the pipelines, edit the assembly, recompile them and use
pipeline replacement to load the edited code.

This patch adds a simpler and more robust way by letting one write
inline assembly in glsl.

glsl allows strings only in a single place, which is the debug print
instruction. Using the special '%ra' format specifier [0] triggers llpc
to interpret the second argument as an inline assembly string.

The arguments to printf are:
- "%ra" for radeon-assembly
- assembly instructions
- variable constraints
- "no output" or a variable where the output is stored (optional if
  there are no further arguments)
- zero or more arguments

See the llvm reference for more details on how to write the constraint
string: https://llvm.org/docs/LangRef.html#inline-assembler-expressions

The added tests contain an example of how inline assembly can be used.

[0]: The list of existing format specifiers does not include a %r:
https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/master/docs/debug_printf.md#debug-printf-format-string
  • Loading branch information
Flakebi committed Apr 12, 2022
1 parent 890379c commit e8b0a67
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 5 deletions.
20 changes: 20 additions & 0 deletions include/khronos/NonSemanticDebugPrintf.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
************************************************************************************************************************
*
* Copyright (C) 2022 Advanced Micro Devices, Inc. All rights reserved.
*
***********************************************************************************************************************/
/**
***********************************************************************************************************************
* @file NonSemanticDebugPrintf.h
* @brief SPIR-V header file: proxy to the real Khronos SPIR-V header.
***********************************************************************************************************************
*/

#pragma once

#if EXTERNAL_SPIRV_HEADERS
#include "spirv/unified1/NonSemanticDebugPrintf.h"
#else
#include "spirv/NonSemanticDebugPrintf.h"
#endif
50 changes: 50 additions & 0 deletions include/khronos/spirv/NonSemanticDebugPrintf.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2020 The Khronos Group Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and/or associated documentation files (the
// "Materials"), to deal in the Materials without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Materials, and to
// permit persons to whom the Materials are furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Materials.
//
// MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS
// KHRONOS STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS
// SPECIFICATIONS AND HEADER INFORMATION ARE LOCATED AT
// https://www.khronos.org/registry/
//
// THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
//

#ifndef SPIRV_UNIFIED1_NonSemanticDebugPrintf_H_
#define SPIRV_UNIFIED1_NonSemanticDebugPrintf_H_

#ifdef __cplusplus
extern "C" {
#endif

enum {
NonSemanticDebugPrintfRevision = 1,
NonSemanticDebugPrintfRevision_BitWidthPadding = 0x7fffffff
};

enum NonSemanticDebugPrintfInstructions {
NonSemanticDebugPrintfDebugPrintf = 1,
NonSemanticDebugPrintfInstructionsMax = 0x7fffffff
};


#ifdef __cplusplus
}
#endif

#endif // SPIRV_UNIFIED1_NonSemanticDebugPrintf_H_
26 changes: 26 additions & 0 deletions llpc/test/shaderdb/extensions/OpExtInst_Print_InlineAsm.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
; RUN: amdllpc -spvgen-dir=%spvgendir% -v %gfxip %s | FileCheck -check-prefix=SHADERTEST %s
; SHADERTEST-LABEL: {{^// LLPC}} SPIRV-to-LLVM translation results
; SHADERTEST: = call float asm sideeffect "v_mul_f32 $0, $1, 2.0", "=v,r"(float
; SHADERTEST: AMDLLPC SUCCESS
*/

#version 450

#extension GL_EXT_debug_printf : enable

layout(binding = 0) uniform Uniforms
{
float x;
};

layout(location = 0) out vec4 fragColor;

void main()
{
float x = x;

debugPrintfEXT("%ra", "v_mul_f32 $0, $1, 2.0", "=v,r", x, x);

fragColor = vec4(x);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
; RUN: not amdllpc -spvgen-dir=%spvgendir% %gfxip %s 2>&1 | FileCheck -check-prefix=SHADERTEST %s
;
; SHADERTEST: Not enough arguments for inline assembly {{.*}} 'my %ra'
*/

#version 450

#extension GL_EXT_debug_printf : enable

layout(location = 0) out vec4 fragColor;

void main()
{
debugPrintfEXT("my %ra");
fragColor = vec4(0.0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
; RUN: not amdllpc -spvgen-dir=%spvgendir% %gfxip %s 2>&1 | FileCheck -check-prefix=SHADERTEST %s
;
; SHADERTEST: Unexpected string 'v_another_inst' {{.*}} Expected a variable or 'no output'
*/

#version 450

#extension GL_EXT_debug_printf : enable

layout(location = 0) out vec4 fragColor;

void main()
{
debugPrintfEXT("%ra", "v_inst", "", "v_another_inst");
fragColor = vec4(0.0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
; RUN: not amdllpc -spvgen-dir=%spvgendir% %gfxip %s 2>&1 | FileCheck -check-prefix=SHADERTEST %s
;
; SHADERTEST: Unexpected value '' {{.*}} Expected a variable
*/

#version 450

#extension GL_EXT_debug_printf : enable

layout(location = 0) out vec4 fragColor;

void main()
{
debugPrintfEXT("%ra", "v_mov $0, 1.0", "", 1);
fragColor = vec4(0.0);
}
116 changes: 113 additions & 3 deletions llpc/translator/lib/SPIRV/SPIRVReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
#include "llvm/IR/Constants.h"
#include "llvm/IR/DIBuilder.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/IR/DiagnosticInfo.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InlineAsm.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/IntrinsicsAMDGPU.h"
Expand Down Expand Up @@ -3778,9 +3780,15 @@ template <> Value *SPIRVToLLVM::transValueWithOpcode<OpGroupFMaxNonUniformAMD>(S
template <> Value *SPIRVToLLVM::transValueWithOpcode<OpExtInst>(SPIRVValue *const spvValue) {
SPIRVExtInst *const spvExtInst = static_cast<SPIRVExtInst *>(spvValue);

// Just ignore this set of extended instructions
if (m_bm->getBuiltinSet(spvExtInst->getExtSetId()) == SPIRVEIS_NonSemanticInfo)
return nullptr;
if (m_bm->getBuiltinSet(spvExtInst->getExtSetId()) == SPIRVEIS_NonSemanticInfo) {
switch (spvExtInst->getExtOp()) {
case NonSemanticDebugPrintfDebugPrintf:
return transDebugPrintf(spvExtInst);
default:
// Just ignore this set of extended instructions
return nullptr;
}
}

std::vector<SPIRVValue *> spvArgValues = spvExtInst->getArgumentValues();

Expand Down Expand Up @@ -8283,6 +8291,108 @@ Value *SPIRVToLLVM::transGLSLExtInst(SPIRVExtInst *extInst, BasicBlock *bb) {
}
}

// =============================================================================
// Translate DebugPrintf extended instruction
Value *SPIRVToLLVM::transDebugPrintf(SPIRVExtInst *spvExtInst) {
auto &spvArgs = spvExtInst->getArguments();
assert(spvArgs.size() >= 1 && "printf must have at least one argument");
auto &formatString = m_bm->get<SPIRVString>(spvArgs[0])->getStr();
// Ignore printfs that don't have a matching format specifiers
if (formatString.find("%r") == std::string::npos)
return nullptr;

if (formatString.find("%ra") != std::string::npos) {
auto *builder = getBuilder();
BasicBlock *const block = builder->GetInsertBlock();
Function *const func = block->getParent();

if (spvArgs.size() < 2) {
m_context->diagnose(DiagnosticInfoUnsupported(
*func,
Twine("Not enough arguments for inline assembly in printf with format string '") + formatString + "'"));
return nullptr;
}

// Handle %ra (radeon-assembly) format specifier:
// The first non-format string argument is a string for the inline assembly.
// The second argument is a string for the constraints.
// The third argument (optional if there are no more arguments) is either a variable to write the output to or the
// string "no output".
// The rest of the arguments are passed as arguments to the inline assembly.
//
// In GLSL, inline assembly can be added like this:
//
// // Enable printf extension
// #extension GL_EXT_debug_printf : enable
//
// // In code:
// float x = 0.0;
// debugPrintfEXT(
// "%ra", // format string containing %ra
// "v_mul_f32 $0, $1, 2.0", // inline assembly string
// "=v,r", // constraints for each variable
// x, // Variable (OpLoad in SPIR-V) ($0) or "no output"
// x // Second argument ($1)
// );
//
// The format of the inline assembly and constraint strings is explained in the LLVM language reference:
// https://llvm.org/docs/LangRef.html#inline-assembler-expressions

SmallVector<Type *> argumentTypes;
SmallVector<Value *> arguments;
Type *returnType = builder->getVoidTy();
Value *returnValue = nullptr;

if (spvArgs.size() > 3) {
auto *returnEntry = m_bm->getEntry(spvArgs[3]);
if (returnEntry->getOpCode() == OpString) {
auto &returnEntryStr = static_cast<SPIRVString *>(returnEntry)->getStr();
if (returnEntryStr != "no output") {
m_context->diagnose(DiagnosticInfoUnsupported(
*func, Twine("Unexpected string '") + returnEntryStr +
"' for return argument in inline assembly. Expected a variable or 'no output'"));
return nullptr;
}
} else {
auto *returnSpvValue = static_cast<SPIRVValue *>(returnEntry);
// Look through OpLoad and store the return value there
if (returnSpvValue->getOpCode() != OpLoad) {
m_context->diagnose(DiagnosticInfoUnsupported(
*func, Twine("Unexpected value '") + returnSpvValue->getName() +
"' for return argument in inline assembly. Expected a variable (OpLoad) or 'no output'"));
return nullptr;
}
auto *loadInst = static_cast<SPIRVLoad *>(returnSpvValue);
returnValue = transValue(loadInst->getSrc(), func, block);
returnType = transType(returnSpvValue->getType());
}
}

for (unsigned i = 4; i < spvArgs.size(); i++) {
auto *argVal = transValue(m_bm->getValue(spvArgs[i]), func, block);
argumentTypes.push_back(argVal->getType());
arguments.push_back(argVal);
}

auto &asmString = m_bm->get<SPIRVString>(spvArgs[1])->getStr();
auto &constraints = m_bm->get<SPIRVString>(spvArgs[2])->getStr();
auto *functionTy = FunctionType::get(returnType, argumentTypes, false);
auto *inlineAsm = InlineAsm::get(functionTy, asmString, constraints, true);

Value *lastInst = builder->CreateCall(inlineAsm, arguments);
// Store return value to given variable
if (returnValue)
lastInst = builder->CreateStore(lastInst, returnValue);

return lastInst;
}

m_context->diagnose(DiagnosticInfoUnsupported(*getBuilder()->GetInsertBlock()->getParent(),
Twine("Unknown printf %r format specifier in string '") + formatString +
"'. Possible values are %ra for assembly."));
return nullptr;
}

// =============================================================================
// Flush denorm to zero if DenormFlushToZero is set in the shader
Value *SPIRVToLLVM::flushDenorm(Value *val) {
Expand Down
1 change: 1 addition & 0 deletions llpc/translator/lib/SPIRV/SPIRVReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class SPIRVToLLVM {
bool deriveStride = false);
unsigned calcShaderBlockSize(SPIRVType *bt, unsigned blockSize, unsigned matrixStride, bool isRowMajor);
Value *transGLSLExtInst(SPIRVExtInst *extInst, BasicBlock *bb);
Value *transDebugPrintf(SPIRVExtInst *spvExtInst);
Value *flushDenorm(Value *val);
Value *transTrinaryMinMaxExtInst(SPIRVExtInst *extInst, BasicBlock *bb);
Value *transGLSLBuiltinFromExtInst(SPIRVExtInst *bc, BasicBlock *bb);
Expand Down
3 changes: 2 additions & 1 deletion llpc/translator/lib/SPIRV/libSPIRV/spirvExt.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@
#ifndef SPIRVEXT_H
#define SPIRVEXT_H

#include "spirv.hpp"
#include "GLSL.std.450.h"
#include "NonSemanticDebugPrintf.h"
#include "spirv.hpp"

namespace spv
{
Expand Down
1 change: 0 additions & 1 deletion llpc/util/llpcShaderModuleHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ unsigned ShaderModuleHelper::trimSpirvDebugInfo(const BinaryData *spvBin, llvm::
unsigned opCode = (codePos[0] & OpCodeMask);
unsigned wordCount = (codePos[0] >> WordCountShift);
switch (opCode) {
case OpString:
case OpSource:
case OpSourceContinued:
case OpSourceExtension:
Expand Down
51 changes: 51 additions & 0 deletions test/amber/inline-asm.amber
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!amber

# RUN: run_amber_test.py --icd %icd %s | FileCheck %s
# REQUIRES: gfx10.1.0

# Test that inline assembly from printf strings is executed.

SHADER vertex vert_shader GLSL
#version 430

layout(location = 0) in vec3 position_in;
layout(location = 0) out vec2 position_out;

void main() {
gl_Position = vec4(position_in, 1.0);
position_out = gl_Position.xy;
}
END

SHADER fragment frag_shader GLSL
#version 430
#extension GL_EXT_debug_printf : enable

layout(location = 0) in vec2 position_in;
layout(location = 0) out vec4 color_out;

void main() {
// FIXME Inline assembly doesn't appear in pipeline dumps, so we can't check for it here
// CHECK-NOT: v_mul_f32 {{.*}}, 2.0
vec2 pos = (position_in + vec2(1.0)) / 2.0;
color_out = vec4(pos, 0.0, 1.0);
float x = color_out.x;
debugPrintfEXT("%ra", "v_mul_f32 $0, $1, 2.0", "=v,r", x, x);
color_out.x = x;
}
END

BUFFER framebuffer FORMAT B8G8R8A8_UNORM

PIPELINE graphics pipeline
ATTACH vert_shader
ATTACH frag_shader
BIND BUFFER framebuffer AS color LOCATION 0
FRAMEBUFFER_SIZE 32 32
END

CLEAR_COLOR pipeline 0 0 0 255
CLEAR pipeline
RUN pipeline DRAW_RECT POS 0 0 SIZE 32 32

EXPECT framebuffer IDX 16 16 SIZE 1 1 EQ_RGB 255 131 0

0 comments on commit e8b0a67

Please sign in to comment.