diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index 1f82ce4a4b..f53d2743c3 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -17,8 +17,6 @@ #include "GSRendererHW.h" #include "GS/GSGL.h" -const float GSRendererHW::SSR_UV_TOLERANCE = 1e-3f; - GSRendererHW::GSRendererHW() : m_width(default_rt_size.x) , m_height(default_rt_size.y) @@ -863,36 +861,30 @@ void GSRendererHW::SwSpriteRender() const bool texture_mapping_enabled = PRIM->TME; - // Setup registers for SW rendering - GIFRegBITBLTBUF bitbltbuf = {}; + const GSVector4i r = m_r; - if (texture_mapping_enabled) - { - bitbltbuf.SBP = m_context->TEX0.TBP0; - bitbltbuf.SBW = m_context->TEX0.TBW; - bitbltbuf.SPSM = m_context->TEX0.PSM; - } - - bitbltbuf.DBP = m_context->FRAME.Block(); - bitbltbuf.DBW = m_context->FRAME.FBW; - bitbltbuf.DPSM = m_context->FRAME.PSM; - - ASSERT(m_r.x == 0 && m_r.y == 0); // No rendering region offset - ASSERT(!PRIM->TME || (abs(m_vt.m_min.t.x) <= SSR_UV_TOLERANCE && abs(m_vt.m_min.t.y) <= SSR_UV_TOLERANCE)); // No input texture offset, if any - ASSERT(!PRIM->TME || (abs(m_vt.m_max.t.x - m_r.z) <= SSR_UV_TOLERANCE && abs(m_vt.m_max.t.y - m_r.w) <= SSR_UV_TOLERANCE)); // No input texture min/mag, if any - ASSERT(!PRIM->TME || (m_vt.m_max.t.x <= (1 << m_context->TEX0.TW) && m_vt.m_max.t.y <= (1 << m_context->TEX0.TH))); // No texture UV wrap, if any + const int tw = 1 << m_context->TEX0.TW; + const int th = 1 << m_context->TEX0.TH; + const float meas_tw = m_vt.m_max.t.x - m_vt.m_min.t.x; + const float meas_th = m_vt.m_max.t.y - m_vt.m_min.t.y; + ASSERT(!PRIM->TME || (abs(meas_tw - r.width()) <= SSR_UV_TOLERANCE && abs(meas_th - r.height()) <= SSR_UV_TOLERANCE)); // No input texture min/mag, if any. + ASSERT(!PRIM->TME || (abs(m_vt.m_min.t.x) <= SSR_UV_TOLERANCE && abs(m_vt.m_min.t.y) <= SSR_UV_TOLERANCE && abs(meas_tw - tw) <= SSR_UV_TOLERANCE && abs(meas_th - th) <= SSR_UV_TOLERANCE)); // No texture UV wrap, if any. GIFRegTRXPOS trxpos = {}; - trxpos.DSAX = 0; - trxpos.DSAY = 0; - trxpos.SSAX = 0; - trxpos.SSAY = 0; + trxpos.DSAX = r.x; + trxpos.DSAY = r.y; + trxpos.SSAX = static_cast(m_vt.m_min.t.x / 2) * 2; // Rounded down to closest even integer. + trxpos.SSAY = static_cast(m_vt.m_min.t.y / 2) * 2; + + ASSERT(r.x % 2 == 0 && r.y % 2 == 0); GIFRegTRXREG trxreg = {}; - trxreg.RRW = m_r.z; - trxreg.RRH = m_r.w; + trxreg.RRW = r.width(); + trxreg.RRH = r.height(); + + ASSERT(r.width() % 2 == 0 && r.height() % 2 == 0); // SW rendering code, mainly taken from GSState::Move(), TRXPOS.DIR{X,Y} management excluded @@ -903,20 +895,16 @@ void GSRendererHW::SwSpriteRender() const int w = trxreg.RRW; const int h = trxreg.RRH; - GL_INS("SwSpriteRender: Dest 0x%x W:%d F:%s, size(%d %d)", bitbltbuf.DBP, bitbltbuf.DBW, psm_str(bitbltbuf.DPSM), w, h); + GL_INS("SwSpriteRender: Dest 0x%x W:%d F:%s, size(%d %d)", m_context->FRAME.Block(), m_context->FRAME.FBW, psm_str(m_context->FRAME.PSM), w, h); - if (texture_mapping_enabled) - InvalidateLocalMem(bitbltbuf, GSVector4i(sx, sy, sx + w, sy + h)); - InvalidateVideoMem(bitbltbuf, GSVector4i(dx, dy, dx + w, dy + h)); - - GSOffset spo = texture_mapping_enabled ? m_mem.GetOffset(bitbltbuf.SBP, bitbltbuf.SBW, bitbltbuf.SPSM) : GSOffset(); - GSOffset dpo = m_mem.GetOffset(bitbltbuf.DBP, bitbltbuf.DBW, bitbltbuf.DPSM); + const GSOffset& spo = m_context->offset.tex; + const GSOffset& dpo = m_context->offset.fb; const bool alpha_blending_enabled = PRIM->ABE; const GSVertex& v = m_vertex.buff[m_index.buff[m_index.tail - 1]]; // Last vertex. const GSVector4i vc = GSVector4i(v.RGBAQ.R, v.RGBAQ.G, v.RGBAQ.B, v.RGBAQ.A) // 0x000000AA000000BB000000GG000000RR - .ps32(); // 0x00AA00BB00GG00RR00AA00BB00GG00RR + .ps32(); // 0x00AA00BB00GG00RR00AA00BB00GG00RR const GSVector4i a_mask = GSVector4i::xff000000().u8to16(); // 0x00FF00000000000000FF000000000000 @@ -925,28 +913,36 @@ void GSRendererHW::SwSpriteRender() const u8 tex0_tfx = m_context->TEX0.TFX; const u8 tex0_tcc = m_context->TEX0.TCC; + const u8 alpha_a = m_context->ALPHA.A; const u8 alpha_b = m_context->ALPHA.B; const u8 alpha_c = m_context->ALPHA.C; + const u8 alpha_d = m_context->ALPHA.D; const u8 alpha_fix = m_context->ALPHA.FIX; + if (texture_mapping_enabled) + m_tc->InvalidateLocalMem(spo, GSVector4i(sx, sy, sx + w, sy + h)); + constexpr bool invalidate_local_mem_before_fb_read = false; + if (invalidate_local_mem_before_fb_read && (alpha_blending_enabled || fb_mask_enabled)) + m_tc->InvalidateLocalMem(dpo, m_r); + for (int y = 0; y < h; y++, ++sy, ++dy) { - auto spa = texture_mapping_enabled ? spo.paMulti(m_mem.m_vm32, sx, sy) : GSOffset::PAPtrHelper(); - auto dpa = dpo.paMulti(m_mem.m_vm32, dx, dy); + const auto& spa = spo.paMulti(m_mem.m_vm32, sx, sy); + const auto& dpa = dpo.paMulti(m_mem.m_vm32, dx, dy); ASSERT(w % 2 == 0); for (int x = 0; x < w; x += 2) { u32* di = dpa.value(x); - ASSERT(*di + 1 == *dpa.value(x + 1)); // Destination pixel pair is adjacent in memory + ASSERT(di + 1 == dpa.value(x + 1)); // Destination pixel pair is adjacent in memory GSVector4i sc; if (texture_mapping_enabled) { - u32* si = spa.value(x); + const u32* si = spa.value(x); // Read 2 source pixel colors - ASSERT((*si + 1) == *spa.value(x + 1)); // Source pixel pair is adjacent in memory + ASSERT(si + 1 == spa.value(x + 1)); // Source pixel pair is adjacent in memory sc = GSVector4i::loadl(si).u8to16(); // 0x00AA00BB00GG00RR00aa00bb00gg00rr // Apply TFX @@ -974,31 +970,14 @@ void GSRendererHW::SwSpriteRender() if (alpha_blending_enabled) { // Blending - ASSERT(m_context->ALPHA.A == 0); - ASSERT(alpha_b == 1 || alpha_b == 2); - ASSERT(m_context->ALPHA.D == 1); - - // Flag C - GSVector4i sc_alpha_vec; - - if (alpha_c == 2) - sc_alpha_vec = GSVector4i(alpha_fix).xxxx().ps32(); - else - sc_alpha_vec = (alpha_c == 0 ? sc : dc0) - .yyww() // 0x00AA00BB00AA00BB00aa00bb00aa00bb - .srl32(16) // 0x000000AA000000AA000000aa000000aa - .ps32() // 0x00AA00AA00aa00aa00AA00AA00aa00aa - .xxyy(); // 0x00AA00AA00AA00AA00aa00aa00aa00aa - - switch (alpha_b) - { - case 1: - dc = sc.sub16(dc0).mul16l(sc_alpha_vec).sra16(7).add16(dc0); // (((Cs - Cd) * C) >> 7) + Cd, must use sra16 due to signed 16 bit values - break; - default: - dc = sc.mul16l(sc_alpha_vec).sra16(7).add16(dc0); // (((Cs - 0) * C) >> 7) + Cd, must use sra16 due to signed 16 bit values - break; - } + const GSVector4i A = alpha_a == 0 ? sc : alpha_a == 1 ? dc0 : GSVector4i::zero(); + const GSVector4i B = alpha_b == 0 ? sc : alpha_b == 1 ? dc0 : GSVector4i::zero(); + const GSVector4i C = alpha_c == 2 ? GSVector4i(alpha_fix).xxxx().ps32() : (alpha_c == 0 ? sc : dc0).yyww() // 0x00AA00BB00AA00BB00aa00bb00aa00bb + .srl32(16) // 0x000000AA000000AA000000aa000000aa + .ps32() // 0x00AA00AA00aa00aa00AA00AA00aa00aa + .xxyy(); // 0x00AA00AA00AA00AA00aa00aa00aa00aa + const GSVector4i D = alpha_d == 0 ? sc : alpha_d == 1 ? dc0 : GSVector4i::zero(); + dc = A.sub16(B).mul16l(C).sra16(7).add16(D); // (((A - B) * C) >> 7) + D, must use sra16 due to signed 16 bit values. // dc alpha channels (dc.u16[3], dc.u16[7]) dirty } else @@ -1026,16 +1005,22 @@ void GSRendererHW::SwSpriteRender() GSVector4i::storel(di, dc); } } + + m_tc->InvalidateVideoMem(dpo, m_r); + m_mem.m_clut.Invalidate(); } -bool GSRendererHW::CanUseSwSpriteRender(bool allow_64x64_sprite) +bool GSRendererHW::CanUseSwSpriteRender() { - const bool r_0_0_64_64 = allow_64x64_sprite ? (m_r == GSVector4i(0, 0, 64, 64)).alltrue() : false; - if (r_0_0_64_64 && !allow_64x64_sprite) // Rendering region 64x64 support is enabled via parameter - return false; - const bool r_0_0_16_16 = (m_r == GSVector4i(0, 0, 16, 16)).alltrue(); - if (!r_0_0_16_16 && !r_0_0_64_64) // Rendering region is 16x16 or 64x64, without offset - return false; + const GSVector4i r = m_r; + if (r.x % 2 != 0 || r.y % 2 != 0) + return false; // Even offset. + const int w = r.width(); + const int h = r.height(); + if (w % 2 != 0 || h % 2 != 0) + return false; // Even size. + if (w > 64 || h > 64) + return false; // Small draw. if (PRIM->PRIM != GS_SPRITE && ((PRIM->IIP && m_vt.m_eq.rgba != 0xffff) || (PRIM->TME && !PRIM->FST && m_vt.m_eq.q != 0x1) @@ -1043,7 +1028,7 @@ bool GSRendererHW::CanUseSwSpriteRender(bool allow_64x64_sprite) return false; if (m_vt.m_primclass != GS_TRIANGLE_CLASS && m_vt.m_primclass != GS_SPRITE_CLASS) // Triangle or sprite class prims return false; - if (PRIM->PRIM != GS_TRIANGLESTRIP && PRIM->PRIM != GS_SPRITE) // Triangle strip or sprite draw + if (PRIM->PRIM != GS_TRIANGLESTRIP && PRIM->PRIM != GS_SPRITE) // Triangle strip or sprite draw return false; if (m_vt.m_primclass == GS_TRIANGLE_CLASS && (PRIM->PRIM != GS_TRIANGLESTRIP || m_vertex.tail != 4)) // If triangle class, strip draw with 4 vertices (two prims, emulating single sprite prim) return false; @@ -1060,15 +1045,18 @@ bool GSRendererHW::CanUseSwSpriteRender(bool allow_64x64_sprite) if (m_context->TEX0.PSM != PSM_PSMCT32) // Input texture format is 32 bit color return false; - if (IsMipMapDraw()) // No mipmapping - return false; - if (abs(m_vt.m_min.t.x) > SSR_UV_TOLERANCE || abs(m_vt.m_min.t.y) > SSR_UV_TOLERANCE) // No horizontal nor vertical offset - return false; - if (abs(m_vt.m_max.t.x - m_r.z) > SSR_UV_TOLERANCE || abs(m_vt.m_max.t.y - m_r.w) > SSR_UV_TOLERANCE) // No texture width or height mag/min + if (IsMipMapDraw()) // No mipmapping. return false; const int tw = 1 << m_context->TEX0.TW; const int th = 1 << m_context->TEX0.TH; - if (m_vt.m_max.t.x > tw || m_vt.m_max.t.y > th) // No UV wrapping + const float meas_tw = m_vt.m_max.t.x - m_vt.m_min.t.x; + const float meas_th = m_vt.m_max.t.y - m_vt.m_min.t.y; + if (abs(m_vt.m_min.t.x) > SSR_UV_TOLERANCE || + abs(m_vt.m_min.t.y) > SSR_UV_TOLERANCE || + abs(meas_tw - tw) > SSR_UV_TOLERANCE || + abs(meas_th - th) > SSR_UV_TOLERANCE) // No UV wrapping. + return false; + if (abs(meas_tw - w) > SSR_UV_TOLERANCE || abs(meas_th - h) > SSR_UV_TOLERANCE) // No texture width or height mag/min. return false; } @@ -1751,7 +1739,14 @@ GSRendererHW::Hacks::Hacks() m_oi_list.push_back(HackEntry(CRC::Jak3, CRC::RegionCount, &GSRendererHW::OI_JakGames)); m_oi_list.push_back(HackEntry(CRC::JakX, CRC::RegionCount, &GSRendererHW::OI_JakGames)); + m_oi_list.push_back(HackEntry(CRC::BurnoutTakedown, CRC::RegionCount, &GSRendererHW::OI_BurnoutGames)); + m_oi_list.push_back(HackEntry(CRC::BurnoutRevenge, CRC::RegionCount, &GSRendererHW::OI_BurnoutGames)); + m_oi_list.push_back(HackEntry(CRC::BurnoutDominator, CRC::RegionCount, &GSRendererHW::OI_BurnoutGames)); + m_oo_list.push_back(HackEntry(CRC::MajokkoALaMode2, CRC::RegionCount, &GSRendererHW::OO_MajokkoALaMode2)); + m_oo_list.push_back(HackEntry(CRC::BurnoutTakedown, CRC::RegionCount, &GSRendererHW::OO_BurnoutGames)); + m_oo_list.push_back(HackEntry(CRC::BurnoutRevenge, CRC::RegionCount, &GSRendererHW::OO_BurnoutGames)); + m_oo_list.push_back(HackEntry(CRC::BurnoutDominator, CRC::RegionCount, &GSRendererHW::OO_BurnoutGames)); m_cu_list.push_back(HackEntry(CRC::MajokkoALaMode2, CRC::RegionCount, &GSRendererHW::CU_MajokkoALaMode2)); m_cu_list.push_back(HackEntry(CRC::TalesOfAbyss, CRC::RegionCount, &GSRendererHW::CU_TalesOfAbyss)); @@ -2011,8 +2006,11 @@ bool GSRendererHW::OI_DBZBTGames(GSTexture* rt, GSTexture* ds, GSTextureCache::S if (t && t->m_from_target) // Avoid slow framebuffer readback return true; + if (!((m_r == GSVector4i(0, 0, 16, 16)).alltrue() || (m_r == GSVector4i(0, 0, 64, 64)).alltrue())) + return true; // Only 16x16 or 64x64 draws. + // Sprite rendering - if (!CanUseSwSpriteRender(true)) + if (!CanUseSwSpriteRender()) return true; SwSpriteRender(); @@ -2235,60 +2233,53 @@ bool GSRendererHW::OI_SonicUnleashed(GSTexture* rt, GSTexture* ds, GSTextureCach bool GSRendererHW::OI_PointListPalette(GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t) { - if (m_vt.m_primclass == GS_POINT_CLASS && !PRIM->TME) + const size_t n_vertices = m_vertex.next; + const int w = m_r.width(); + const int h = m_r.height(); + if (m_vt.m_primclass == GS_POINT_CLASS && w <= 64 // Small draws. + && h <= 64 // Small draws. + && n_vertices <= 256 // Small draws. + && PRIM->ABE // Alpha blending. + && !PRIM->TME // No texturing please. + && m_context->FRAME.PSM == PSM_PSMCT32 // Only 32-bit pixel format (CLUT format). + && !PRIM->FGE // No FOG. + && !PRIM->AA1 // No antialiasing. + && !PRIM->FIX // Normal fragment value control. + && !m_env.DTHE.DTHE // No dithering. + && !m_context->TEST.ATE // No alpha test. + && !m_context->TEST.DATE // No destination alpha test. + && (!m_context->DepthRead() && !m_context->DepthWrite()) // No depth handling. + && !m_context->TEX0.CSM // No CLUT usage. + && !m_env.PABE.PABE // No PABE. + && m_context->FBA.FBA == 0 // No Alpha Correction. + && m_context->FRAME.FBMSK == 0 // No frame buffer masking. + && m_context->ALPHA.A == m_context->ALPHA.B // (A - B) == 0 in blending equation, makes C value irrelevant. + && m_context->ALPHA.D == 0 // Copy source RGB(A) color into frame buffer. + ) { const u32 FBP = m_context->FRAME.Block(); const u32 FBW = m_context->FRAME.FBW; - - if (FBP >= 0x03f40 && (FBP & 0x1f) == 0) + GL_INS("PointListPalette - m_r = <%d, %d => %d, %d>, n_vertices = %zu, FBP = 0x%x, FBW = %u", m_r.x, m_r.y, m_r.z, m_r.w, n_vertices, FBP, FBW); + const GSVertex* RESTRICT v = m_vertex.buff; + const int ox(m_context->XYOFFSET.OFX); + const int oy(m_context->XYOFFSET.OFY); + for (size_t i = 0; i < n_vertices; ++i) { - if (m_vertex.next == 16) - { - GSVertex* RESTRICT v = m_vertex.buff; - - for (int i = 0; i < 16; i++, v++) - { - u32 c = v->RGBAQ.U32[0]; - const u32 a = c >> 24; - - c = (a >= 0x80 ? 0xff000000 : (a << 25)) | (c & 0x00ffffff); - - v->RGBAQ.U32[0] = c; - - m_mem.WritePixel32(i & 7, i >> 3, c, FBP, FBW); - } - - m_mem.m_clut.Invalidate(); - - return false; - } - else if (m_vertex.next == 256) - { - GSVertex* RESTRICT v = m_vertex.buff; - - for (int i = 0; i < 256; i++, v++) - { - u32 c = v->RGBAQ.U32[0]; - const u32 a = c >> 24; - - c = (a >= 0x80 ? 0xff000000 : (a << 25)) | (c & 0x00ffffff); - - v->RGBAQ.U32[0] = c; - - m_mem.WritePixel32(i & 15, i >> 4, c, FBP, FBW); - } - - m_mem.m_clut.Invalidate(); - - return false; - } - else - { - ASSERT(0); - } + const GSVertex& vi = v[i]; + const GIFRegXYZ& xyz = vi.XYZ; + const int x = (int(xyz.X) - ox) / 16; + const int y = (int(xyz.Y) - oy) / 16; + if (x < m_r.x || x > m_r.z) + continue; + if (y < m_r.y || y > m_r.w) + continue; + const u32 c = vi.RGBAQ.U32[0]; + m_mem.WritePixel32(x, y, c, FBP, FBW); } + m_tc->InvalidateVideoMem(m_context->offset.fb, m_r); + m_mem.m_clut.Invalidate(); + return false; } - return true; } @@ -2358,7 +2349,10 @@ bool GSRendererHW::OI_ArTonelico2(GSTexture* rt, GSTexture* ds, GSTextureCache:: bool GSRendererHW::OI_JakGames(GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t) { - if (!CanUseSwSpriteRender(false)) + if (!(m_r == GSVector4i(0, 0, 16, 16)).alltrue()) + return true; // Only 16x16 draws. + + if (!CanUseSwSpriteRender()) return true; // Render 16x16 palette via CPU. @@ -2367,6 +2361,23 @@ bool GSRendererHW::OI_JakGames(GSTexture* rt, GSTexture* ds, GSTextureCache::Sou return false; // Skip current draw. } +bool GSRendererHW::OI_BurnoutGames(GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t) +{ + if (!OI_PointListPalette(rt, ds, t)) + return false; // Render point list palette. + + if (t && t->m_from_target) // Avoid slow framebuffer readback + return true; + + if (!CanUseSwSpriteRender()) + return true; + + // Render palette via CPU. + SwSpriteRender(); + + return false; +} + // OO (others output?) hacks: invalidate extra local memory after the draw call void GSRendererHW::OO_MajokkoALaMode2() @@ -2387,6 +2398,41 @@ void GSRendererHW::OO_MajokkoALaMode2() } } +void GSRendererHW::OO_BurnoutGames() +{ + const GIFRegTEX0& TEX0 = m_context->TEX0; + const GIFRegALPHA& ALPHA = m_context->ALPHA; + const GIFRegFRAME& FRAME = m_context->FRAME; + if (PRIM->PRIM == GS_SPRITE + && !PRIM->IIP + && PRIM->TME + && !PRIM->FGE + && PRIM->ABE + && !PRIM->AA1 + && !PRIM->FST + && !PRIM->FIX + && TEX0.TBW == 16 + && TEX0.TW == 10 + && TEX0.TCC + && !TEX0.TFX + && TEX0.PSM == PSM_PSMT8 + && TEX0.CPSM == PSM_PSMCT32 + && !TEX0.CSM + && TEX0.TH == 8 + && ALPHA.A == ALPHA.B + && ALPHA.D == 0 + && FRAME.FBW == 16 + && FRAME.PSM == PSM_PSMCT32) + { + // Readback clouds being rendered during level loading. + // Later the alpha channel from the 32 bit frame buffer is used as an 8 bit indexed texture to draw + // the clouds on top of the sky at each frame. + // Burnout 3 PAL 50Hz: 0x3ba0 => 0x1e80. + GL_INS("OO_BurnoutGames - Readback clouds renderered from TEX0.TBP0 = 0x%04x (TEX0.CBP = 0x%04x) to FBP = 0x%04x", TEX0.TBP0, TEX0.CBP, FRAME.Block()); + m_tc->InvalidateLocalMem(m_context->offset.fb, m_r); + } +} + // Can Upscale hacks: disable upscaling for some draw calls bool GSRendererHW::CU_MajokkoALaMode2() diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.h b/pcsx2/GS/Renderers/HW/GSRendererHW.h index 2ae25779bd..c53b9d83d6 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.h +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.h @@ -35,7 +35,7 @@ private: bool m_userhacks_enabled_gs_mem_clear; bool m_userHacks_merge_sprite; - static const float SSR_UV_TOLERANCE; + static constexpr float SSR_UV_TOLERANCE = 1.0f; #pragma region hacks @@ -59,8 +59,10 @@ private: bool OI_SuperManReturns(GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t); bool OI_ArTonelico2(GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t); bool OI_JakGames(GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t); + bool OI_BurnoutGames(GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t); void OO_MajokkoALaMode2(); + void OO_BurnoutGames(); bool CU_MajokkoALaMode2(); bool CU_TalesOfAbyss(); @@ -136,7 +138,7 @@ private: float alpha0(int L, int X0, int X1); float alpha1(int L, int X0, int X1); void SwSpriteRender(); - bool CanUseSwSpriteRender(bool allow_64x64_sprite); + bool CanUseSwSpriteRender(); template void RoundSpriteOffset();