aboutsummaryrefslogtreecommitdiff
path: root/lib/compilers/rga.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compilers/rga.ts')
-rw-r--r--lib/compilers/rga.ts266
1 files changed, 266 insertions, 0 deletions
diff --git a/lib/compilers/rga.ts b/lib/compilers/rga.ts
new file mode 100644
index 000000000..d3b8b399d
--- /dev/null
+++ b/lib/compilers/rga.ts
@@ -0,0 +1,266 @@
+// 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 {readdir, readFile, rename, writeFile} from 'fs-extra';
+
+import {CompilationResult, ExecutionOptions} from '../../types/compilation/compilation.interfaces';
+import {ParseFilters} from '../../types/features/filters.interfaces';
+import {BaseCompiler} from '../base-compiler';
+import * as exec from '../exec';
+import {logger} from '../logger';
+
+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(`compiler.${this.compiler.id}.dxcPath`);
+ logger.debug(`RGA compiler ${this.compiler.id} configured to use DXC at ${this.dxcPath}`);
+ }
+
+ override optionsForFilter(filters: ParseFilters, 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);
+ result.inputFilename = inputFilename;
+ const transformedInput = result.filenameTransform(inputFilename);
+ this.parseCompilationOutput(result, transformedInput);
+ return result;
+ }
+
+ 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 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 readdir(outputDir, {encoding: 'utf-8'});
+ for (const file of files) {
+ if (file.startsWith((asicSelection.asic as string) + '_output')) {
+ await 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 readFile(outputFile, 'utf-8');
+ const asmLines = asm.split(/\r?\n/);
+ const analysis = await readFile(registerAnalysisFile, 'utf-8');
+ 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;
+ }
+
+ outputAsm.push(`; ${analysisLines[i + analysisOffset]}`, asmLines[i + asmOffset]);
+ }
+
+ await 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 = `RGA didn't emit expected ISA output.`;
+ return rgaResult;
+ }
+}