From c383d049ecd8e89fad4a72567ad66151d7572e1c Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Mon, 30 Mar 2026 21:58:35 -0700 Subject: [PATCH] [CPU] Implement `AND`+`NOT` folding into `AND_NOT` Detect dependent `AND` and `NOT` IR sequences and combine them into a singular `AND_NOT` opcode. The later dead-code-elimination-pass will get rid of the left-over `NOT` opcode if nothing else uses it. This gets quite a good amount of hits in some of the titles I've tested. Also updates unit tests with additional data-types and ensures that `And(..., Not())` returns the same result as `AndNot(...)` --- .../compiler/passes/simplification_pass.cc | 36 ++++ .../cpu/compiler/passes/simplification_pass.h | 1 + src/xenia/cpu/testing/opcode_coverage_test.cc | 171 ++++++++++++++++++ 3 files changed, 208 insertions(+) diff --git a/src/xenia/cpu/compiler/passes/simplification_pass.cc b/src/xenia/cpu/compiler/passes/simplification_pass.cc index d7fdf246a..2779f682a 100644 --- a/src/xenia/cpu/compiler/passes/simplification_pass.cc +++ b/src/xenia/cpu/compiler/passes/simplification_pass.cc @@ -568,6 +568,10 @@ bool SimplificationPass::TryHandleANDROLORSHLSeq(hir::Instr* i, bool SimplificationPass::CheckAnd(hir::Instr* i, hir::HIRBuilder* builder) { retry_and_simplification: + if (SimplifyAndNot(i, builder)) { + return true; + } + auto [constant_value, variable_value] = i->BinaryValueArrangeAsConstAndVar(); if (!constant_value) { // added this for srawi @@ -1247,6 +1251,38 @@ bool SimplificationPass::SimplifyAddArith(hir::Instr* i, return false; } +bool SimplificationPass::SimplifyAndNot(hir::Instr* i, + hir::HIRBuilder* builder) { + // check if either of the 2 AND operands has just used NOT and fold into + // an AND_NOT opcode + Value* src1 = i->src1.value; + Value* src2 = i->src2.value; + + Instr* def1 = src1->def; + Instr* def2 = src2->def; + if (!def1 || !def2) return false; + + // Bypass the NOT from an incoming operand and combine it into AND_NOT. + // If the original NOT does not have any further uses, then the + // dead-code-elimination pass will delete it. Otherwise, if it still has uses, + // then there will still be a NOT operation. + if (def2->opcode == &OPCODE_NOT_info) { + // Fold src2's NOT into AND_NOT + i->Replace(&OPCODE_AND_NOT_info, 0); + i->set_src1(src1); + i->set_src2(def2->src1.value); + return true; + } else if (def1->opcode == &OPCODE_NOT_info) { + // Swap operands and fold src1's NOT into AND_NOT + i->Replace(&OPCODE_AND_NOT_info, 0); + i->set_src1(src2); + i->set_src2(def1->src1.value); + return true; + } + + return false; +} + bool SimplificationPass::SimplifySubArith(hir::Instr* i, hir::HIRBuilder* builder) { /* diff --git a/src/xenia/cpu/compiler/passes/simplification_pass.h b/src/xenia/cpu/compiler/passes/simplification_pass.h index 1e69951e5..d9c8fc092 100644 --- a/src/xenia/cpu/compiler/passes/simplification_pass.h +++ b/src/xenia/cpu/compiler/passes/simplification_pass.h @@ -42,6 +42,7 @@ class SimplificationPass : public ConditionalGroupSubpass { bool SimplifyAddWithSHL(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifyAddToSelf(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifyAddArith(hir::Instr* i, hir::HIRBuilder* builder); + bool SimplifyAndNot(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifySubArith(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifySHLArith(hir::Instr* i, hir::HIRBuilder* builder); // handle either or or xor with 0 diff --git a/src/xenia/cpu/testing/opcode_coverage_test.cc b/src/xenia/cpu/testing/opcode_coverage_test.cc index db1ece2ea..c70b051ab 100644 --- a/src/xenia/cpu/testing/opcode_coverage_test.cc +++ b/src/xenia/cpu/testing/opcode_coverage_test.cc @@ -491,8 +491,100 @@ TEST_CASE("ATOMIC_COMPARE_EXCHANGE_I32", "[atomic]") { // ============================================================================ // AND_NOT — bitwise AND with complement of second operand // ============================================================================ +TEST_CASE("AND_NOT_I8", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT8_TYPE))), + INT64_TYPE)); + StoreGPR(b, 3, + b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Truncate(LoadGPR(b, 5), INT8_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF; + ctx->r[5] = 0x0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0xF0); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAA; + ctx->r[5] = 0xFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x00); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x12; + ctx->r[5] = 0x00; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x12); + }); +} + +TEST_CASE("AND_NOT_I16", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT16_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT16_TYPE))), + INT64_TYPE)); + StoreGPR(b, 3, + b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT16_TYPE), + b.Truncate(LoadGPR(b, 5), INT16_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF00; + ctx->r[5] = 0x0F0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0xF000); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAAAA; + ctx->r[5] = 0xFFFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x0000); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x1234; + ctx->r[5] = 0x0000; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x1234); + }); +} + TEST_CASE("AND_NOT_I32", "[bitwise]") { TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT32_TYPE))), + INT64_TYPE)); StoreGPR(b, 3, b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT32_TYPE), b.Truncate(LoadGPR(b, 5), INT32_TYPE)), @@ -506,6 +598,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0x0F0F0F0F; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0xF000F000); }); // All bits masked out. @@ -515,6 +608,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0xFFFFFFFF; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0x00000000); }); // No bits masked out. @@ -524,10 +618,87 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0x00000000; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0x12345678); }); } +TEST_CASE("AND_NOT_I64", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, b.And(LoadGPR(b, 4), b.Not(LoadGPR(b, 5)))); + StoreGPR(b, 3, b.AndNot(LoadGPR(b, 4), LoadGPR(b, 5))); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF00FF00FF00FF00; + ctx->r[5] = 0x0F0F0F0F0F0F0F0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0xF000F000F000F000); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAAAAAAAAAAAAAAAA; + ctx->r[5] = 0xFFFFFFFFFFFFFFFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0x0000000000000000); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x1234567812345678; + ctx->r[5] = 0x0000000000000000; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0x1234567812345678); + }); +} + +TEST_CASE("AND_NOT_V128", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreVR(b, 2, b.And(LoadVR(b, 4), b.Not(LoadVR(b, 5)))); + StoreVR(b, 3, b.AndNot(LoadVR(b, 4), LoadVR(b, 5))); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128s(0xFF00); + ctx->v[5] = vec128s(0x0F0F); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128s(0xF000)); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128b(0xAA); + ctx->v[5] = vec128b(0xFF); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128b(0x00)); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128i(0x12345678); + ctx->v[5] = vec128i(0x00000000); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128i(0x12345678)); + }); +} + // ============================================================================ // TRUNCATE — integer narrowing // ============================================================================