blob: 21d839ccd28bbc1a4ca6ad203540de0a4253d295 [file] [log] [blame] [raw]
// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import path from 'path';
import fs from 'fs-extra';
import type {CompilationResult, ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import * as exec from '../exec.js';
import {logger} from '../logger.js';
interface ASICSelection {
asic?: string;
error?: string;
printASICs?: boolean;
}
// RGA := Radeon GPU Analyzer
export class RGACompiler extends BaseCompiler {
private dxcPath: string;
static get key() {
return 'rga';
}
constructor(info: any, env: any) {
super(info, env);
this.compiler.supportsIntel = false;
this.dxcPath = this.compilerProps<string>(`compiler.${this.compiler.id}.dxcPath`);
logger.debug(`RGA compiler ${this.compiler.id} configured to use DXC at ${this.dxcPath}`);
}
override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: any, userOptions?: any): any[] {
return [outputFilename];
}
extractASIC(dxcArgs: string[]): ASICSelection {
// Scan dxc args for an `--asic` argument that should be stripped and passed later to RGA
// Default to RDNA2
let asic = 'gfx1030';
let printASICs = true;
for (let i = 0; i !== dxcArgs.length; ++i) {
const arg = dxcArgs[i];
if (arg === '--asic') {
// NOTE: the last arguments are the input source file and -spirv, so check
// if --asic immediately precedes that
if (i === dxcArgs.length - 3) {
return {
error: '--asic flag supplied without subsequent ASIC!',
};
}
asic = dxcArgs[i + 1];
// Do a quick sanity check to determine if a valid ASIC was supplied
if (!asic.startsWith('gfx')) {
return {
error: `The argument immediately following --asic doesn't appear to be a valid ASIC.
Please supply an ASIC from the following options:`,
printASICs: true,
};
}
// Remove these two arguments from the dxcArgs list
dxcArgs.splice(i, 2);
// If the user supplied a specific ASIC, don't bother printing available ASIC options
printASICs = false;
break;
}
}
return {
asic,
printASICs,
};
}
execTime(startTime: bigint, endTime: bigint): string {
return ((endTime - startTime) / BigInt(1000000)).toString();
}
override async runCompiler(
compiler: string,
options: string[],
inputFilename: string,
execOptions: ExecutionOptions,
): Promise<CompilationResult> {
if (!execOptions) {
execOptions = this.getDefaultExecOptions();
}
if (!execOptions.customCwd) {
execOptions.customCwd = path.dirname(inputFilename);
}
const result = await this.execDXCandRGA(compiler, options, execOptions);
return this.transformToCompilationResult(result, inputFilename);
}
async execDXCandRGA(filepath: string, args: string[], execOptions: ExecutionOptions): Promise<any> {
// RGA is invoked in two steps. First, DXC is invoked to compile the SPIR-V output of the HLSL file.
// Next, RGA is invoked to consume the SPIR-V output and produce the requested ISA.
// Track the total time spent instead of relying on executeDirect's internal timing facility
const startTime = process.hrtime.bigint();
// The first argument is the target output file
const outputFile = args[0];
const outputDir = path.dirname(outputFile);
const spvTemp = 'output.spv.txt';
logger.debug(`Intermediate SPIR-V output: ${spvTemp}`);
const dxcArgs = args.slice(1);
if (!dxcArgs.includes('-spirv')) {
dxcArgs.push('-spirv');
}
logger.debug(`DXC args: ${dxcArgs}`);
const asicSelection = this.extractASIC(dxcArgs);
if (asicSelection.error) {
// Invalid user ASIC selected, bail out immediately
const endTime = process.hrtime.bigint();
// Synthesize a faux-execution result (see promise resolution code in executeDirect)
return {
code: -1,
okToCache: true,
filenameTransform: x => x,
stdout: asicSelection.error,
execTime: this.execTime(startTime, endTime),
};
}
const dxcResult = await exec.execute(this.dxcPath, dxcArgs, execOptions);
if (dxcResult.code !== 0) {
// Failed to compile SPIR-V intermediate product. Exit immediately with DXC invocation result.
const endTime = process.hrtime.bigint();
dxcResult.execTime = this.execTime(startTime, endTime);
return dxcResult;
}
try {
await fs.writeFile(path.join(outputDir, spvTemp), dxcResult.stdout);
} catch (e) {
const endTime = process.hrtime.bigint();
return {
code: -1,
okToCache: true,
filenameTransform: x => x,
stdout: 'Failed to emit intermediate SPIR-V result.',
execTime: this.execTime(startTime, endTime),
};
}
let registerAnalysisFile = 'livereg.txt';
const rgaArgs = [
'-s',
'vk-spv-txt-offline',
'-c',
asicSelection.asic || '',
'--isa',
outputFile,
'--livereg',
registerAnalysisFile,
spvTemp,
];
logger.debug(`RGA args: ${rgaArgs}`);
const rgaResult = await exec.execute(filepath, rgaArgs, execOptions);
if (rgaResult.code !== 0) {
// Failed to compile AMD ISA
const endTime = process.hrtime.bigint();
rgaResult.execTime = this.execTime(startTime, endTime);
return rgaResult;
}
// RGA doesn't emit the exact file we requested. It prepends the requested GPU
// architecture and appends the shader type (with underscore separators). Here,
// we rename the generated file to the output file Compiler Explorer expects.
const files = await fs.readdir(outputDir, {encoding: 'utf8'});
for (const file of files) {
if (file.startsWith((asicSelection.asic as string) + '_output')) {
await fs.rename(path.join(outputDir, file), outputFile);
registerAnalysisFile = path.join(outputDir, file.replace('output', 'livereg').replace('.s', '.txt'));
// The register analysis file contains a legend, and register liveness data
// for each line of disassembly. Interleave those lines into the final output
// as assembly comments.
const asm = await fs.readFile(outputFile, 'utf8');
const asmLines = asm.split(/\r?\n/);
const analysis = await fs.readFile(registerAnalysisFile, 'utf8');
const analysisLines = analysis.split(/\r?\n/);
// The first few lines of the register analysis are the legend. Emit those lines
// as comments at the start of the output.
let analysisOffset = analysisLines.indexOf('');
analysisOffset += 3;
const epilogueOffset = analysisLines.indexOf('', analysisOffset);
const outputAsm = analysisLines.slice(epilogueOffset + 1).map(line => `; ${line}`);
outputAsm.push(...analysisLines.slice(0, analysisOffset).map(line => `; ${line}`), '\n');
let asmOffset = asmLines.indexOf('');
outputAsm.push(...asmLines.slice(0, asmOffset));
asmOffset += 1;
// Perform the interleave
for (let i = 0; i !== asmOffset + asmLines.length; ++i) {
if (i + analysisOffset >= epilogueOffset) {
outputAsm.push(...asmLines.slice(i));
break;
}
// Check if this line of assembly corresponds to a label. If so, emit the asm line
// and continue from the next line (the register analysis file operates on instructions,
// and labels are not considered instructions)
if (asmLines[i + asmOffset].startsWith('label')) {
outputAsm.push(asmLines[i + asmOffset]);
++asmOffset;
--i;
continue;
}
outputAsm.push(`; ${analysisLines[i + analysisOffset]}`, asmLines[i + asmOffset]);
}
await fs.writeFile(outputFile, outputAsm.join('\n'));
if (asicSelection.printASICs) {
rgaResult.stdout += `ISA compiled with the default AMD ASIC (Radeon RX 6800 series RDNA2).
To override this, pass --asic [ASIC] to the options above (nonstandard DXC option),
where [ASIC] corresponds to one of the following options:`;
const asics = await exec.execute(filepath, ['-s', 'vk-spv-txt-offline', '-l'], execOptions);
rgaResult.stdout += '\n';
rgaResult.stdout += asics.stdout;
}
const endTime = process.hrtime.bigint();
rgaResult.execTime = this.execTime(startTime, endTime);
return rgaResult;
}
}
// Arriving here means the expected ISA result wasn't emitted. Synthesize an error.
const endTime = process.hrtime.bigint();
rgaResult.execTime = this.execTime(startTime, endTime);
rgaResult.stdout += `\nRGA didn't emit expected ISA output.`;
return rgaResult;
}
}