pcsx2/plugins/GSdx/GSRendererSW.h

859 lines
19 KiB
C++

/*
* Copyright (C) 2007-2009 Gabest
* http://www.gabest.org
*
* This Program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This Program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GNU Make; see the file COPYING. If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
* http://www.gnu.org/copyleft/gpl.html
*
*/
#pragma once
#include "GSRenderer.h"
#include "GSTextureCacheSW.h"
#include "GSDrawScanline.h"
extern const GSVector4 g_pos_scale;
template <class Device>
class GSRendererSW : public GSRendererT<Device, GSVertexSW>
{
protected:
GSRasterizerList m_rl;
GSTextureCacheSW* m_tc;
GSVertexTrace m_vtrace;
Texture m_texture[2];
bool m_reset;
void Reset()
{
// TODO: GSreset can come from the main thread too => crash
// m_tc->RemoveAll();
m_reset = true;
__super::Reset();
}
void VSync(int field)
{
__super::VSync(field);
m_tc->IncAge();
if(m_reset)
{
m_tc->RemoveAll();
m_reset = false;
}
// if((m_perfmon.GetFrame() & 255) == 0) m_rl.PrintStats();
}
void ResetDevice()
{
m_texture[0] = Texture();
m_texture[1] = Texture();
}
bool GetOutput(int i, Texture& t)
{
CRect r(0, 0, DISPFB[i]->FBW * 64, GetFrameRect(i).bottom);
// TODO: round up bottom
if(m_texture[i].GetWidth() != r.Width() || m_texture[i].GetHeight() != r.Height())
{
m_texture[i] = Texture();
}
if(!m_texture[i] && !m_dev.CreateTexture(m_texture[i], r.Width(), r.Height()))
{
return false;
}
GIFRegTEX0 TEX0;
TEX0.TBP0 = DISPFB[i]->Block();
TEX0.TBW = DISPFB[i]->FBW;
TEX0.PSM = DISPFB[i]->PSM;
GIFRegCLAMP CLAMP;
CLAMP.WMS = CLAMP.WMT = 1;
// TODO
static BYTE* buff = (BYTE*)_aligned_malloc(1024 * 1024 * 4, 16);
static int pitch = 1024 * 4;
m_mem.ReadTexture(r, buff, pitch, TEX0, m_env.TEXA, CLAMP);
m_texture[i].Update(r, buff, pitch);
t = m_texture[i];
if(s_dump)
{
CString str;
str.Format(_T("c:\\temp1\\_%05d_f%I64d_fr%d_%05x_%d.bmp"), s_n++, m_perfmon.GetFrame(), i, (int)TEX0.TBP0, (int)TEX0.PSM);
if(s_save) t.Save(str);
}
return true;
}
bool TryAlphaTest(DWORD& fm, DWORD& zm)
{
const GSDrawingEnvironment& env = m_env;
const GSDrawingContext* context = m_context;
bool pass = true;
if(context->TEST.ATST == ATST_NEVER)
{
pass = false;
}
else if(context->TEST.ATST != ATST_ALWAYS)
{
GSVector4i af = GSVector4i(m_vtrace.m_min.c.wwww(m_vtrace.m_max.c)) >> 7;
int amin, amax;
if(PRIM->TME && (context->TEX0.TCC || context->TEX0.TFX == TFX_DECAL))
{
DWORD bpp = GSLocalMemory::m_psm[context->TEX0.PSM].trbpp;
DWORD cbpp = GSLocalMemory::m_psm[context->TEX0.CPSM].trbpp;
DWORD pal = GSLocalMemory::m_psm[context->TEX0.PSM].pal;
if(bpp == 32)
{
return false;
}
else if(bpp == 24)
{
amin = env.TEXA.AEM ? 0 : env.TEXA.TA0;
amax = env.TEXA.TA0;
}
else if(bpp == 16)
{
amin = env.TEXA.AEM ? 0 : min(env.TEXA.TA0, env.TEXA.TA1);
amax = max(env.TEXA.TA0, env.TEXA.TA1);
}
else
{
m_mem.m_clut.GetAlphaMinMax32(amin, amax);
}
switch(context->TEX0.TFX)
{
case TFX_MODULATE:
amin = (amin * af.x) >> 7;
amax = (amax * af.z) >> 7;
if(amin > 255) amin = 255;
if(amax > 255) amax = 255;
break;
case TFX_DECAL:
break;
case TFX_HIGHLIGHT:
amin = amin + af.x;
amax = amax + af.z;
if(amin > 255) amin = 255;
if(amax > 255) amax = 255;
break;
case TFX_HIGHLIGHT2:
break;
default:
__assume(0);
}
}
else
{
amin = af.x;
amax = af.z;
}
int aref = context->TEST.AREF;
switch(context->TEST.ATST)
{
case ATST_NEVER:
pass = false;
break;
case ATST_ALWAYS:
pass = true;
break;
case ATST_LESS:
if(amax < aref) pass = true;
else if(amin >= aref) pass = false;
else return false;
break;
case ATST_LEQUAL:
if(amax <= aref) pass = true;
else if(amin > aref) pass = false;
else return false;
break;
case ATST_EQUAL:
if(amin == aref && amax == aref) pass = true;
else if(amin > aref || amax < aref) pass = false;
else return false;
break;
case ATST_GEQUAL:
if(amin >= aref) pass = true;
else if(amax < aref) pass = false;
else return false;
break;
case ATST_GREATER:
if(amin > aref) pass = true;
else if(amax <= aref) pass = false;
else return false;
break;
case ATST_NOTEQUAL:
if(amin == aref && amax == aref) pass = false;
else if(amin > aref || amax < aref) pass = true;
else return false;
break;
default:
__assume(0);
}
}
if(!pass)
{
switch(context->TEST.AFAIL)
{
case AFAIL_KEEP: fm = zm = 0xffffffff; break;
case AFAIL_FB_ONLY: zm = 0xffffffff; break;
case AFAIL_ZB_ONLY: fm = 0xffffffff; break;
case AFAIL_RGB_ONLY: fm |= 0xff000000; zm = 0xffffffff; break;
default: __assume(0);
}
}
return true;
}
void GetScanlineParam(GSScanlineParam& p, GS_PRIM_CLASS primclass)
{
const GSDrawingEnvironment& env = m_env;
const GSDrawingContext* context = m_context;
p.vm = m_mem.m_vm32;
p.fbo = m_mem.GetOffset(context->FRAME.Block(), context->FRAME.FBW, context->FRAME.PSM);
p.zbo = m_mem.GetOffset(context->ZBUF.Block(), context->FRAME.FBW, context->ZBUF.PSM);
p.fzbo = m_mem.GetOffset4(context->FRAME, context->ZBUF);
p.sel.dw = 0;
p.sel.fpsm = 3;
p.sel.zpsm = 3;
p.sel.atst = ATST_ALWAYS;
p.sel.tfx = TFX_NONE;
p.sel.abe = 255;
p.sel.sprite = primclass == GS_SPRITE_CLASS ? 1 : 0;
p.fm = context->FRAME.FBMSK;
p.zm = context->ZBUF.ZMSK || context->TEST.ZTE == 0 ? 0xffffffff : 0;
if(context->TEST.ZTE && context->TEST.ZTST == ZTST_NEVER)
{
p.fm = 0xffffffff;
p.zm = 0xffffffff;
}
if(PRIM->TME)
{
m_mem.m_clut.Read32(context->TEX0, env.TEXA);
}
if(context->TEST.ATE)
{
if(!TryAlphaTest(p.fm, p.zm))
{
p.sel.atst = context->TEST.ATST;
p.sel.afail = context->TEST.AFAIL;
}
}
bool fwrite = p.fm != 0xffffffff;
bool ftest = p.sel.atst != ATST_ALWAYS || context->TEST.DATE && context->FRAME.PSM != PSM_PSMCT24;
if(fwrite || ftest)
{
p.sel.fpsm = GSUtil::EncodePSM(context->FRAME.PSM);
if((primclass == GS_LINE_CLASS || primclass == GS_TRIANGLE_CLASS) && m_vtrace.m_eq.rgba != 15)
{
p.sel.iip = PRIM->IIP;
}
if(PRIM->TME)
{
p.sel.tfx = context->TEX0.TFX;
p.sel.tcc = context->TEX0.TCC;
p.sel.fst = PRIM->FST;
p.sel.ltf = context->TEX1.IsLinear();
p.sel.tlu = GSLocalMemory::m_psm[context->TEX0.PSM].pal > 0;
if(p.sel.iip == 0 && p.sel.tfx == TFX_MODULATE && p.sel.tcc)
{
if(m_vtrace.m_eq.rgba == 15 && (m_vtrace.m_min.c == GSVector4(128.0f * 128.0f)).alltrue())
{
// modulate does not do anything when vertex color is 0x80
p.sel.tfx = TFX_DECAL;
}
}
if(p.sel.tfx == TFX_DECAL)
{
p.sel.tcc = 1;
}
if(p.sel.fst == 0)
{
// skip per pixel division if q is constant
GSVertexSW* v = m_vertices;
if(m_vtrace.m_eq.q)
{
p.sel.fst = 1;
if(v[0].t.z != 1.0f)
{
GSVector4 w = v[0].t.zzzz().rcpnr();
for(int i = 0, j = m_count; i < j; i++)
{
v[i].t *= w;
}
m_vtrace.m_min.t *= w;
m_vtrace.m_max.t *= w;
}
}
else if(primclass == GS_SPRITE_CLASS)
{
p.sel.fst = 1;
GSVector4 tmin = GSVector4(FLT_MAX);
GSVector4 tmax = GSVector4(-FLT_MAX);
for(int i = 0, j = m_count; i < j; i += 2)
{
GSVector4 w = v[i + 1].t.zzzz().rcpnr();
GSVector4 v0 = v[i + 0].t * w;
GSVector4 v1 = v[i + 1].t * w;
v[i + 0].t = v0;
v[i + 1].t = v1;
tmin = tmin.minv(v0).minv(v1);
tmax = tmax.maxv(v0).maxv(v1);
}
m_vtrace.m_max.t = tmax;
m_vtrace.m_min.t = tmin;
}
}
if(p.sel.fst)
{
// if q is constant we can do the half pel shift for bilinear sampling on the vertices
if(p.sel.ltf)
{
GSVector4 half(0x8000, 0x8000);
GSVertexSW* v = m_vertices;
for(int i = 0, j = m_count; i < j; i++)
{
v[i].t -= half;
}
m_vtrace.m_min.t -= half;
m_vtrace.m_max.t += half;
}
}
/*
else
{
GSVector4 tmin = GSVector4(FLT_MAX);
GSVector4 tmax = GSVector4(-FLT_MAX);
GSVertexSW* v = m_vertices;
for(int i = 0, j = m_count; i < j; i++)
{
GSVector4 v0 = v[i].t * v[i].t.zzzz().rcpnr();
tmin = tmin.minv(v0);
tmax = tmax.maxv(v0);
}
if(p.sel.ltf)
{
GSVector4 half(0x8000, 0x8000);
tmin -= half;
tmax += half;
}
m_vtrace.min.t = tmin;
m_vtrace.max.t = tmax;
}
*/
CRect r;
int w = 1 << context->TEX0.TW;
int h = 1 << context->TEX0.TH;
MinMaxUV(w, h, r, p.sel.fst);
const GSTextureCacheSW::GSTexture* t = m_tc->Lookup(context->TEX0, env.TEXA, &r);
if(!t) {ASSERT(0); return;}
p.tex = t->m_buff;
p.clut = m_mem.m_clut;
p.tw = t->m_tw;
}
p.sel.fge = PRIM->FGE;
if(context->FRAME.PSM != PSM_PSMCT24)
{
p.sel.date = context->TEST.DATE;
}
if(PRIM->ABE)
{
if(!context->ALPHA.IsOpaque())
{
p.sel.abe = context->ALPHA.ai32[0];
p.sel.pabe = env.PABE.PABE;
}
}
if(PRIM->AA1)
{
// TODO: automatic alpha blending (ABE=1, A=0 B=1 C=0 D=1)
}
if(p.sel.date
|| p.sel.abea == 1 || p.sel.abeb == 1 || p.sel.abec == 1 || p.sel.abed == 1
|| p.sel.atst != ATST_ALWAYS && p.sel.afail == AFAIL_RGB_ONLY
|| p.sel.fpsm == 0 && p.fm != 0 && p.fm != 0xffffffff
|| p.sel.fpsm == 1 && (p.fm & 0x00ffffff) != 0 && (p.fm & 0x00ffffff) != 0x00ffffff
|| p.sel.fpsm == 2 && (p.fm & 0x80f8f8f8) != 0 && (p.fm & 0x80f8f8f8) != 0x80f8f8f8)
{
p.sel.rfb = 1;
}
}
bool zwrite = p.zm != 0xffffffff;
bool ztest = context->TEST.ZTE && context->TEST.ZTST > 1;
if(zwrite || ztest)
{
p.sel.zpsm = GSUtil::EncodePSM(context->ZBUF.PSM);
p.sel.ztst = ztest ? context->TEST.ZTST : 1;
}
}
void Draw()
{
m_vtrace.Update(m_vertices, m_count);
GS_PRIM_CLASS primclass = GSUtil::GetPrimClass(PRIM->PRIM);
GSScanlineParam p;
GetScanlineParam(p, primclass);
if((p.fm & p.zm) == 0xffffffff)
{
return;
}
if(s_dump)
{
CString str;
str.Format(_T("c:\\temp1\\_%05d_f%I64d_tex_%05x_%d.bmp"), s_n++, m_perfmon.GetFrame(), (int)m_context->TEX0.TBP0, (int)m_context->TEX0.PSM);
if(PRIM->TME) if(s_save) {m_mem.SaveBMP(str, m_context->TEX0.TBP0, m_context->TEX0.TBW, m_context->TEX0.PSM, 1 << m_context->TEX0.TW, 1 << m_context->TEX0.TH);}
str.Format(_T("c:\\temp1\\_%05d_f%I64d_rt0_%05x_%d.bmp"), s_n++, m_perfmon.GetFrame(), m_context->FRAME.Block(), m_context->FRAME.PSM);
if(s_save) {m_mem.SaveBMP(str, m_context->FRAME.Block(), m_context->FRAME.FBW, m_context->FRAME.PSM, GetFrameSize(1).cx, 512);}//GetFrameSize(1).cy);
str.Format(_T("c:\\temp1\\_%05d_f%I64d_rz0_%05x_%d.bmp"), s_n-1, m_perfmon.GetFrame(), m_context->ZBUF.Block(), m_context->ZBUF.PSM);
if(s_savez) {m_mem.SaveBMP(str, m_context->ZBUF.Block(), m_context->FRAME.FBW, m_context->ZBUF.PSM, GetFrameSize(1).cx, 512);}
}
GSRasterizerData data;
data.scissor = GSVector4i(m_context->scissor.in);
data.scissor.z = min(data.scissor.z, (int)m_context->FRAME.FBW * 64); // TODO: find a game that overflows and check which one is the right behaviour
data.primclass = primclass;
data.vertices = m_vertices;
data.count = m_count;
data.param = &p;
m_rl.Draw(&data);
GSRasterizerStats stats;
m_rl.GetStats(stats);
m_perfmon.Put(GSPerfMon::Draw, 1);
m_perfmon.Put(GSPerfMon::Prim, stats.prims);
m_perfmon.Put(GSPerfMon::Fillrate, stats.pixels);
GSVector4i pos(m_vtrace.m_min.p.xyxy(m_vtrace.m_max.p));
GSVector4i scissor = data.scissor;
CRect r;
r.left = max(scissor.x, min(scissor.z, pos.x));
r.top = max(scissor.y, min(scissor.w, pos.y));
r.right = max(scissor.x, min(scissor.z, pos.z));
r.bottom = max(scissor.y, min(scissor.w, pos.w));
GIFRegBITBLTBUF BITBLTBUF;
BITBLTBUF.DBW = m_context->FRAME.FBW;
if(p.fm != 0xffffffff)
{
BITBLTBUF.DBP = m_context->FRAME.Block();
BITBLTBUF.DPSM = m_context->FRAME.PSM;
m_tc->InvalidateVideoMem(BITBLTBUF, r);
}
if(p.zm != 0xffffffff)
{
BITBLTBUF.DBP = m_context->ZBUF.Block();
BITBLTBUF.DPSM = m_context->ZBUF.PSM;
m_tc->InvalidateVideoMem(BITBLTBUF, r);
}
if(s_dump)
{
CString str;
str.Format(_T("c:\\temp1\\_%05d_f%I64d_rt1_%05x_%d.bmp"), s_n++, m_perfmon.GetFrame(), m_context->FRAME.Block(), m_context->FRAME.PSM);
if(s_save) {m_mem.SaveBMP(str, m_context->FRAME.Block(), m_context->FRAME.FBW, m_context->FRAME.PSM, GetFrameSize(1).cx, 512);}//GetFrameSize(1).cy);
str.Format(_T("c:\\temp1\\_%05d_f%I64d_rz1_%05x_%d.bmp"), s_n-1, m_perfmon.GetFrame(), m_context->ZBUF.Block(), m_context->ZBUF.PSM);
if(s_savez) {m_mem.SaveBMP(str, m_context->ZBUF.Block(), m_context->FRAME.FBW, m_context->ZBUF.PSM, GetFrameSize(1).cx, 512);}
}
}
void InvalidateVideoMem(const GIFRegBITBLTBUF& BITBLTBUF, CRect r)
{
m_tc->InvalidateVideoMem(BITBLTBUF, r);
}
void MinMaxUV(int w, int h, CRect& r, DWORD fst)
{
const GSDrawingContext* context = m_context;
int wms = context->CLAMP.WMS;
int wmt = context->CLAMP.WMT;
int minu = (int)context->CLAMP.MINU;
int minv = (int)context->CLAMP.MINV;
int maxu = (int)context->CLAMP.MAXU;
int maxv = (int)context->CLAMP.MAXV;
GSVector4i vr(0, 0, w, h);
switch(wms)
{
case CLAMP_REPEAT:
break;
case CLAMP_CLAMP:
break;
case CLAMP_REGION_CLAMP:
if(vr.x < minu) vr.x = minu;
if(vr.z > maxu + 1) vr.z = maxu + 1;
break;
case CLAMP_REGION_REPEAT:
vr.x = maxu;
vr.z = vr.x + (minu + 1);
break;
default:
__assume(0);
}
switch(wmt)
{
case CLAMP_REPEAT:
break;
case CLAMP_CLAMP:
break;
case CLAMP_REGION_CLAMP:
if(vr.y < minv) vr.y = minv;
if(vr.w > maxv + 1) vr.w = maxv + 1;
break;
case CLAMP_REGION_REPEAT:
vr.y = maxv;
vr.w = vr.y + (minv + 1);
break;
default:
__assume(0);
}
if(fst)
{
GSVector4i uv = GSVector4i(m_vtrace.m_min.t.xyxy(m_vtrace.m_max.t)).sra32(16);
/*
int tw = context->TEX0.TW;
int th = context->TEX0.TH;
GSVector4i u = uv & GSVector4i::xffffffff().srl32(32 - tw);
GSVector4i v = uv & GSVector4i::xffffffff().srl32(32 - th);
GSVector4i uu = uv.sra32(tw);
GSVector4i vv = uv.sra32(th);
int mask = (uu.upl32(vv) == uu.uph32(vv)).mask();
*/
switch(wms)
{
case CLAMP_REPEAT:
/*
if(mask & 0x000f)
{
if(vr.x < u.x) vr.x = u.x;
if(vr.z > u.z + 1) vr.z = u.z + 1;
}
*/
break;
case CLAMP_CLAMP:
case CLAMP_REGION_CLAMP:
if(vr.x < uv.x) vr.x = uv.x;
if(vr.z > uv.z + 1) vr.z = uv.z + 1;
break;
case CLAMP_REGION_REPEAT: // TODO
break;
default:
__assume(0);
}
switch(wmt)
{
case CLAMP_REPEAT:
/*
if(mask & 0xf000)
{
if(vr.y < v.y) vr.y = v.y;
if(vr.w > v.w + 1) vr.w = v.w + 1;
}
*/
break;
case CLAMP_CLAMP:
case CLAMP_REGION_CLAMP:
if(vr.y < uv.y) vr.y = uv.y;
if(vr.w > uv.w + 1) vr.w = uv.w + 1;
break;
case CLAMP_REGION_REPEAT: // TODO
break;
default:
__assume(0);
}
}
r = vr;
r &= CRect(0, 0, w, h);
}
public:
GSRendererSW(BYTE* base, bool mt, void (*irq)(), int nloophack, const GSRendererSettings& rs, int threads)
: GSRendererT(base, mt, irq, nloophack, rs)
{
m_rl.Create<GSDrawScanline>(this, threads);
m_tc = new GSTextureCacheSW(this);
InitVertexKick<GSRendererSW<Device> >();
}
virtual ~GSRendererSW()
{
delete m_tc;
}
template<DWORD prim, DWORD tme, DWORD fst>
void VertexKick(bool skip)
{
const GSDrawingContext* context = m_context;
GSVector4i xy = GSVector4i::load((int)m_v.XYZ.ai32[0]);
xy = xy.insert16<3>(m_v.FOG.F);
xy = xy.upl16();
xy -= context->XYOFFSET;
GSVertexSW v;
v.p = GSVector4(xy) * g_pos_scale;
v.c = GSVector4(GSVector4i::load((int)m_v.RGBAQ.ai32[0]).u8to32() << 7);
if(tme)
{
float q;
if(fst)
{
v.t = GSVector4(((GSVector4i)m_v.UV).upl16() << (16 - 4));
q = 1.0f;
}
else
{
v.t = GSVector4(m_v.ST.S, m_v.ST.T);
v.t *= GSVector4(0x10000 << context->TEX0.TW, 0x10000 << context->TEX0.TH);
q = m_v.RGBAQ.Q;
}
v.t = v.t.xyxy(GSVector4::load(q));
}
GSVertexSW& dst = m_vl.AddTail();
dst = v;
dst.p.z = (float)min(m_v.XYZ.Z, 0xffffff00); // max value which can survive the DWORD => float => DWORD conversion
DWORD count = 0;
if(GSVertexSW* v = DrawingKick<prim>(skip, count))
{
GSVector4 pmin, pmax;
switch(prim)
{
case GS_POINTLIST:
pmin = v[0].p;
pmax = v[0].p;
break;
case GS_LINELIST:
case GS_LINESTRIP:
case GS_SPRITE:
pmin = v[0].p.minv(v[1].p);
pmax = v[0].p.maxv(v[1].p);
break;
case GS_TRIANGLELIST:
case GS_TRIANGLESTRIP:
case GS_TRIANGLEFAN:
pmin = v[0].p.minv(v[1].p).minv(v[2].p);
pmax = v[0].p.maxv(v[1].p).maxv(v[2].p);
break;
}
GSVector4 scissor = context->scissor.ex;
GSVector4 test = (pmax < scissor) | (pmin > scissor.zwxy());
switch(prim)
{
case GS_TRIANGLELIST:
case GS_TRIANGLESTRIP:
case GS_TRIANGLEFAN:
case GS_SPRITE:
test |= pmin.ceil() == pmax.ceil();
break;
}
if(test.mask() & 3)
{
return;
}
switch(prim)
{
case GS_POINTLIST:
break;
case GS_LINELIST:
case GS_LINESTRIP:
if(PRIM->IIP == 0) {v[0].c = v[1].c;}
break;
case GS_TRIANGLELIST:
case GS_TRIANGLESTRIP:
case GS_TRIANGLEFAN:
if(PRIM->IIP == 0) {v[0].c = v[2].c; v[1].c = v[2].c;}
break;
case GS_SPRITE:
break;
}
if(m_count >= 3 && m_count < 30)
{
GSVertexSW* v = &m_vertices[m_count - 3];
int tl = 0;
int br = 0;
bool isquad = false;
switch(prim)
{
case GS_TRIANGLESTRIP:
case GS_TRIANGLEFAN:
case GS_TRIANGLELIST:
isquad = GSVertexSW::IsQuad(v, tl, br);
break;
}
if(isquad)
{
m_count -= 3;
if(m_count > 0)
{
tl += m_count;
br += m_count;
Flush();
}
if(tl != 0) m_vertices[0] = m_vertices[tl];
if(br != 1) m_vertices[1] = m_vertices[br];
m_count = 2;
UINT32 tmp = PRIM->PRIM;
PRIM->PRIM = GS_SPRITE;
Flush();
PRIM->PRIM = tmp;
m_perfmon.Put(GSPerfMon::Quad, 1);
return;
}
}
m_count += count;
}
}
};