From 88a54e2d989e5956dd344a532605424bdb890f07 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 30 Nov 2025 02:29:41 +0300 Subject: [PATCH] rsx/fp: Add a basic assembler to aid in test authoring --- rpcs3/Emu/RSX/Program/Assembler/FPASM.cpp | 301 ++++++++++++++++++++++ rpcs3/Emu/RSX/Program/Assembler/FPASM.h | 28 ++ rpcs3/emucore.vcxproj | 2 + rpcs3/emucore.vcxproj.filters | 6 + rpcs3/tests/rpcs3_test.vcxproj | 1 + rpcs3/tests/test_rsx_fp_asm.cpp | 33 +++ 6 files changed, 371 insertions(+) create mode 100644 rpcs3/Emu/RSX/Program/Assembler/FPASM.cpp create mode 100644 rpcs3/Emu/RSX/Program/Assembler/FPASM.h create mode 100644 rpcs3/tests/test_rsx_fp_asm.cpp diff --git a/rpcs3/Emu/RSX/Program/Assembler/FPASM.cpp b/rpcs3/Emu/RSX/Program/Assembler/FPASM.cpp new file mode 100644 index 0000000000..f8fcb3c571 --- /dev/null +++ b/rpcs3/Emu/RSX/Program/Assembler/FPASM.cpp @@ -0,0 +1,301 @@ +#include "stdafx.h" +#include "FPASM.h" +#include "Emu/RSX/Program/RSXFragmentProgram.h" + +#ifndef _WIN32 +#define sscanf_s sscanf +#endif + +namespace rsx::assembler +{ + Instruction* FPIR::load(const RegisterRef& ref, int operand, Instruction* prev) + { + Instruction* target = prev; + if (!target) + { + m_instructions.push_back({}); + target = &m_instructions.back(); + } + + SRC_Common src{ .HEX = target->bytecode[operand + 1] }; + src.reg_type = RSX_FP_REGISTER_TYPE_TEMP; + src.fp16 = ref.reg.f16 ? 1 : 0; + src.tmp_reg_index = static_cast(ref.reg.id); + + src.swizzle_x = 0; + src.swizzle_y = 1; + src.swizzle_z = 2; + src.swizzle_w = 3; + + target->bytecode[operand + 1] = src.HEX; + return target; + } + + Instruction* FPIR::load(const std::array& constants, int operand, Instruction* prev) + { + Instruction* target = prev; + if (!target) + { + m_instructions.push_back({}); + target = &m_instructions.back(); + } + + // Unsupported for now + ensure(target->length == 4, "FPIR cannot encode more than one constant load per instruction"); + + SRC_Common src{ .HEX = target->bytecode[operand + 1] }; + src.reg_type = RSX_FP_REGISTER_TYPE_CONSTANT; + target->bytecode[operand + 1] = src.HEX; + + src.swizzle_x = 0; + src.swizzle_y = 1; + src.swizzle_z = 2; + src.swizzle_w = 3; + + // Embed literal constant + std::memcpy(&target->bytecode[4], constants.data(), 4 * sizeof(u32)); + target->length = 8; + return target; + } + + Instruction* FPIR::store(const RegisterRef& ref, Instruction* prev) + { + Instruction* target = prev; + if (!target) + { + m_instructions.push_back({}); + target = &m_instructions.back(); + } + + OPDEST dst{ .HEX = target->bytecode[0] }; + dst.dest_reg = static_cast(ref.reg.id); + dst.fp16 = ref.reg.f16 ? 1 : 0; + dst.write_mask = ref.mask; + dst.prec = ref.reg.f16 ? RSX_FP_PRECISION_HALF : RSX_FP_PRECISION_REAL; + + target->bytecode[0] = dst.HEX; + return target; + } + + void FPIR::mov(const RegisterRef& dst, f32 constant) + { + Instruction* inst = store(dst); + inst = load(std::array{ constant, constant, constant, constant }, 0); + inst->opcode = RSX_FP_OPCODE_MOV; + } + + void FPIR::mov(const RegisterRef& dst, const RegisterRef& src) + { + Instruction* inst = store(dst); + inst = load(src, 0); + inst->opcode = RSX_FP_OPCODE_MOV; + } + + void FPIR::add(const RegisterRef& dst, const std::array& constants) + { + Instruction* inst = store(dst); + inst = load(constants, 0); + inst->opcode = RSX_FP_OPCODE_ADD; + } + + void FPIR::add(const RegisterRef& dst, const RegisterRef& src) + { + Instruction* inst = store(dst); + inst = load(src, 0); + inst->opcode = RSX_FP_OPCODE_ADD; + } + + std::vector FPIR::build() + { + return m_instructions; + } + + FPIR FPIR::from_source(const std::string& asm_) + { + std::vector instructions = fmt::split(asm_, { "\n", ";" }); + if (instructions.empty()) + { + return {}; + } + + auto transform_inst = [](const std::string& s) + { + std::string result; + result.reserve(s.size()); + + bool literal = false; + for (auto& c : s) + { + if (c == ' ') + { + if (!literal && !result.empty() && result.back() != ',') + { + result += ','; // Replace token separator space with comma + } + continue; + } + + if (std::isspace(c)) + { + continue; + } + + if (!literal && c == '{') + { + literal = true; + } + + if (literal && c == '}') + { + literal = false; + } + + if (c == ',') + { + result += (literal ? '|' : ','); + continue; + } + + result += c; + } + return result; + }; + + auto decode_instruction = [&](const std::string& inst, std::string& op, std::string& dst, std::vector& sources) + { + const auto i = transform_inst(inst); + if (i.empty()) + { + return; + } + + const auto tokens = fmt::split(i, { "," }); + ensure(!tokens.empty(), "Invalid input"); + + op = tokens.front(); + + if (tokens.size() > 1) + { + dst = tokens[1]; + } + + for (size_t n = 2; n < tokens.size(); ++n) + { + sources.push_back(tokens[n]); + } + }; + + auto get_ref = [](const std::string& reg) + { + ensure(reg.length() > 1, "Invalid register specifier"); + + const auto index = std::stoi(reg.substr(1)); + RegisterRef ref + { + .reg { .id = index, .f16 = false }, + .mask = 0x0F + }; + + if (reg[0] == 'H' || reg[0] == 'h') + { + ref.reg.f16 = true; + } + + return ref; + }; + + auto get_constants = [](const std::string& reg) -> std::array + { + float x, y, z, w; + if (sscanf_s(reg.c_str(), "#{%f|%f|%f|%f}", &x, &y, &z, &w) == 4) + { + return { x, y, z, w }; + } + + if (sscanf_s(reg.c_str(), "#{%f}", &x) == 1) + { + return { x, x, x, x }; + } + + fmt::throw_exception("Invalid constant literal"); + }; + + auto encode_opcode = [](const std::string& op, Instruction* inst) + { + OPDEST d0 { .HEX = inst->bytecode[0] }; + + if (op == "MOV") + { + inst->opcode = d0.opcode = RSX_FP_OPCODE_MOV; + inst->bytecode[0] = d0.HEX; + return; + } + + if (op == "ADD") + { + inst->opcode = d0.opcode = RSX_FP_OPCODE_ADD; + inst->bytecode[0] = d0.HEX; + return; + } + + fmt::throw_exception("Unhandled instruction '%s'", op); + }; + + std::string op, dst; + std::vector sources; + + FPIR ir{}; + + for (const auto& instruction : instructions) + { + op.clear(); + dst.clear(); + sources.clear(); + decode_instruction(instruction, op, dst, sources); + + if (op.empty()) + { + continue; + } + + ir.m_instructions.push_back({}); + Instruction* target = &ir.m_instructions.back(); + + encode_opcode(op, target); + + if (dst.empty()) + { + OPDEST dst{}; + dst.no_dest = 1; + target->bytecode[0] = dst.HEX; + } + else + { + ir.store(get_ref(dst), target); + } + + int operand = 0; + for (const auto& source : sources) + { + if (source.front() == '#') + { + const auto literal = get_constants(source); + ir.load(literal, operand++, target); + continue; + } + + ir.load(get_ref(source), operand++, target); + } + } + + if (!ir.m_instructions.empty()) + { + OPDEST d0{ .HEX = ir.m_instructions.back().bytecode[0] }; + d0.end = 1; + + ir.m_instructions.back().bytecode[0] = d0.HEX; + } + + return ir; + } +} diff --git a/rpcs3/Emu/RSX/Program/Assembler/FPASM.h b/rpcs3/Emu/RSX/Program/Assembler/FPASM.h new file mode 100644 index 0000000000..c30b1560b6 --- /dev/null +++ b/rpcs3/Emu/RSX/Program/Assembler/FPASM.h @@ -0,0 +1,28 @@ +#pragma once + +#include "IR.h" + +namespace rsx::assembler +{ + class FPIR + { + public: + void mov(const RegisterRef& dst, f32 constant); + void mov(const RegisterRef& dst, const RegisterRef& src); + + void add(const RegisterRef& dst, const std::array& constants); + void add(const RegisterRef& dst, const RegisterRef& src); + + std::vector build(); + + static FPIR from_source(const std::string& asm_); + + private: + Instruction* load(const RegisterRef& reg, int operand, Instruction* target = nullptr); + Instruction* load(const std::array& constants, int operand, Instruction* target = nullptr); + Instruction* store(const RegisterRef& reg, Instruction* target = nullptr); + + std::vector m_instructions; + }; +} + diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 1b46634019..761f42dd07 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -156,6 +156,7 @@ + @@ -704,6 +705,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 94cc8c5aed..937b22f5cc 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -1393,6 +1393,9 @@ Emu\GPU\RSX\Program\Assembler + + Emu\GPU\RSX\Program\Assembler + @@ -2800,6 +2803,9 @@ Emu\GPU\RSX\Program\Assembler\Passes\FP + + Emu\GPU\RSX\Program\Assembler + diff --git a/rpcs3/tests/rpcs3_test.vcxproj b/rpcs3/tests/rpcs3_test.vcxproj index 22992e6a07..2851f2faa6 100644 --- a/rpcs3/tests/rpcs3_test.vcxproj +++ b/rpcs3/tests/rpcs3_test.vcxproj @@ -89,6 +89,7 @@ + diff --git a/rpcs3/tests/test_rsx_fp_asm.cpp b/rpcs3/tests/test_rsx_fp_asm.cpp new file mode 100644 index 0000000000..1fe899e925 --- /dev/null +++ b/rpcs3/tests/test_rsx_fp_asm.cpp @@ -0,0 +1,33 @@ +#include + +#include "Emu/RSX/Common/simple_array.hpp" +#include "Emu/RSX/Program/Assembler/FPASM.h" +#include "Emu/RSX/Program/RSXFragmentProgram.h" + +namespace rsx::assembler +{ + TEST(TestFPIR, FromSource) + { + auto ir = FPIR::from_source(R"( + MOV R0, #{ 0.125 }; + ADD R1, R0; + )"); + + const auto instructions = ir.build(); + + ASSERT_EQ(instructions.size(), 2); + + EXPECT_EQ(OPDEST{ .HEX = instructions[0].bytecode[0] }.end, 0); + EXPECT_EQ(OPDEST{ .HEX = instructions[0].bytecode[0] }.opcode, RSX_FP_OPCODE_MOV); + EXPECT_EQ(SRC0{ .HEX = instructions[0].bytecode[1] }.reg_type, RSX_FP_REGISTER_TYPE_CONSTANT); + EXPECT_EQ(OPDEST{ .HEX = instructions[0].bytecode[0] }.opcode, RSX_FP_OPCODE_MOV); + EXPECT_EQ(instructions[0].length, 8); + + EXPECT_EQ(OPDEST{ .HEX = instructions[1].bytecode[0] }.end, 1); + EXPECT_EQ(OPDEST{ .HEX = instructions[1].bytecode[0] }.opcode, RSX_FP_OPCODE_ADD); + EXPECT_EQ(OPDEST{ .HEX = instructions[1].bytecode[0] }.dest_reg, 1); + EXPECT_EQ(OPDEST{ .HEX = instructions[1].bytecode[0] }.fp16, 0); + EXPECT_EQ(SRC0{ .HEX = instructions[1].bytecode[1] }.reg_type, RSX_FP_REGISTER_TYPE_TEMP); + EXPECT_EQ(instructions[1].length, 4); + } +}