//////////////////////////////////////////////////////////////////////////////
//
// Asura_Platform_Decal.cpp
//
// Copyright (c) 2004-2005 Rebellion. All rights reserved.
//
//////////////////////////////////////////////////////////////////////////////

// I N C L U D E S ///////////////////////////////////////////////////////////

#include "Core/Asura.h"

// This file's header.
#include "SpecialFX/Asura_Platform_Decal.h"

// Engine independent.
#include "Camera/Asura_Camera.h"
#include "Core/Asura_Timers.h"
#include "Rendering/Asura_Colour.h"
#include "SpecialFX/Asura_Decal_Template.h"
#include "SpecialFX/Asura_Decal_System.h"

// Engine PS2.
#include "Core/Asura_PS2_DMA.h"
#include "Rendering/Asura_PlatformRendering.h"
#include "Rendering/Asura_PlatformRenderStates.h"
#include "Rendering/Asura_PlatformTextureManagement.h"
#include "Rendering/Asura_PS2_DeferredRendering.h"
#include "Rendering/Asura_PS2_GS.h"
#include "Rendering/Asura_PS2_RenderingOptions.h"
#include "Rendering/Asura_PS2_VU1.h"

// M A C R O S ///////////////////////////////////////////////////////////////

// See CreateDMAChain(). This is
//
//      1           (STCYCL)
//    + 1 + 4 * v   (positions)
//    + 1 + v       (STs)
//    + 1 + v       (RGBAs)
//    + 2           (ITOP, MSCNT)
//
#define CALC_CHAIN_SIZE_WORDS( num_verts ) \
    ( 6 + 6 * (num_verts) )

#define CALC_CHAIN_SIZE_QWS( num_verts ) \
    ( (CALC_CHAIN_SIZE_WORDS(num_verts) + 3) >> 2 )

// The output buffer is 244 and 3 QW's per vertex
#define MAX_DECAL_VERTS ( ( 244 - 1 ) / 3 )

// C O N S T A N T S /////////////////////////////////////////////////////////

// (MP - 05.11.2005) This used to be 81 but the bigger this value is the
// more noticeable it is when we drop a buffer. The obvious trade off is that
// if this is too small then it will be less efficient. I don't think
// reducing it to 50 should have an impact in that respect.
static const u_int uMAX_VERTS_PER_BATCH = 50;

static const u_int uPER_CAM_DATA_SIZE  = 8;
static const u_int uINPUT_BFR_START    = 0x210;
static const u_int uINPUT_BFR_SIZE     = 0xF3;
static const u_int uMAX_CHAIN_SIZE_QWS = CALC_CHAIN_SIZE_QWS( uMAX_VERTS_PER_BATCH );

static const u_int uDECAL_FLAGS_WE_CARE_ABOUT = ASURA_DECAL_DETAILMAP;

// C L A S S E S /////////////////////////////////////////////////////////////

struct DecalVtxWithColour
{
    float fX, fY, fZ; u_int uADC;

    short sS, sT;

    u_int uABGR;
};

class DecalBufferList
{

public:

    // C L A S S   M E T H O D S /////////////////////////////////////////////

    static void CreateAll( const Asura_PS2_DecalSystem::BufferListSpec* pxSpecs, u_int uNumSpecs );

    static void DeleteAll( void );

    static DecalBufferList* FindList( int iTexID, u_int uDecalFlags );

    static u_int GetNumLists( void ) { return s_uNumLists; }

    static void Render( void );

    // M E T H O D S /////////////////////////////////////////////////////////

    void AppendToScratchBfr( const Asura_Decal_Vertex* pxStrip, u_int uNumVerts, const Asura_RGBA& xDiffuseRGBA );

    inline bool CanFitInScratchBfr( u_int uNumVerts ) const
    {
        return( m_uNumVertsInScratch + uNumVerts <= uMAX_VERTS_PER_BATCH );
    }

    bool FlushScratchBfr( void );

    inline Asura_PS2_QW* GetChainAddr( u_int uChainIdx ) const
    {
        return m_pxDMAChains + uChainIdx * uMAX_CHAIN_SIZE_QWS;
    }

    inline bool IsScratchBfrFull( void ) const
    {
        // We shouldn't ever get less than 4 vertices, ie. a single quad,
        // coming in as a strip so let's ensure we can at least fit that in.
        return( m_uNumVertsInScratch + 4 > uMAX_VERTS_PER_BATCH );
    }

private:

    // M E T H O D S /////////////////////////////////////////////////////////

    ~DecalBufferList( void );

    void Initialise( const Asura_PS2_DecalSystem::BufferListSpec& xSpec, Asura_PS2_QW* pxChainWorkspace );

    // C L A S S   S T A T E /////////////////////////////////////////////////

    static DecalBufferList* s_pxAllLists;
    static u_int            s_uNumLists;

    // S T A T E /////////////////////////////////////////////////////////////

    int   m_iTexID;
    u_int m_uDecalFlags;

    DecalVtxWithColour m_axScratchBfr[uMAX_VERTS_PER_BATCH];
    u_int              m_uNumVertsInScratch;
#if ASURA_PS2_COLLECT_RENDER_STATS
    u_int              m_uNumTrisInScratch;
#endif

    Asura_PS2_QW* m_pxDMAChains;
    u_int         m_uNumDMAChains;
    u_short*      m_pusQWCs;
#if ASURA_PS2_COLLECT_RENDER_STATS
    u_char*       m_pucTriCounts;
#endif
    u_int         m_uFirstDMAChain;
    u_int         m_uSpareDMAChain;
    u_int         m_uFrameAtWhichSpareChainIsValid;

};
// End class DecalBufferList.

// P R O T O T Y P E S   A N D   I N L I N E   F U N C T I O N S /////////////

static u_int CreateDMAChain( const DecalVtxWithColour* pxDecalVerts, u_int uNumVerts, Asura_PS2_QW* pxOut );

// C L A S S   I M P L E M E N T A T I O N S /////////////////////////////////

//
// Asura_Decal_System.
//

void Asura_Decal_System::Platform_Initialise( void )
{
    // Query platform code for required buffer lists.
    const Asura_PS2_DecalSystem::BufferListSpec* pxSpecs;
    u_int uNumSpecs;
    Asura_PS2_DecalSystem::Project_GetBufferListSpecs( &pxSpecs, &uNumSpecs );

    // And pre-allocate them.
    DecalBufferList::CreateAll( pxSpecs, uNumSpecs );
}

void Asura_Decal_System::Platform_Reset( void )
{
}

void Asura_Decal_System::Platform_Shutdown( void )
{
    DecalBufferList::DeleteAll();
}

void Asura_Decal_Handler::Platform_AddStrip( const Asura_Decal_Vertex* pxStrip,
                                            u_int uNumVerts,
                                            const Asura_RGBA& xDiffuseRGBA,
                                            const Asura_Decal_Template* pxDecalTemplate )
{
    Asura_Assert( uNumVerts >= 3, "" );
    
    // It's going to be hopelessly inefficient if strips end up larger than
    // half the buffer size.
    Asura_Assert( uNumVerts <= (uMAX_VERTS_PER_BATCH >> 1), "" );
    // (And note that of course it's not going to work at all if there are
    // more than will be rendered in a batch.)

    DecalBufferList* pxList = DecalBufferList::FindList( pxDecalTemplate->m_iTextureIndex, pxDecalTemplate->m_uFlags );

    // That should never fail, assuming project code is doing the right thing.
    Asura_Assert( pxList, "" );

    // Will this strip make the buffer wrap?
    bool bCanFit = pxList->CanFitInScratchBfr( uNumVerts );
    if( !bCanFit )
    {
        // Try flushing the scratch buffer then. This may fail if we've done
        // it too recently for this buffer list. In that case we have no
        // choice but to ignore the request to add this decal.
        if( !pxList->FlushScratchBfr() )
        {
            return;
        }
    }

    pxList->AppendToScratchBfr( pxStrip, uNumVerts, xDiffuseRGBA );

    // If we've now filled that buffer, try and flush it immediately, rather
    // than waiting until we try and fail to append to it later. This should
    // make it less likely that we'll ever fail to flush above and have to
    // drop a decal. Note that there's no point in trying to do this if we
    // already flushed above.
    if( bCanFit && pxList->IsScratchBfrFull() )
    {
        pxList->FlushScratchBfr();
    }
}

void Asura_Decal_Handler::Platform_Render(void)
{
    if( !Asura_PS2_RenderingOptions::GetOption(Asura_PS2_RenderingOptions::OPTION_DRAW_DECALS) || !DecalBufferList::GetNumLists() )
    {
        return;
    }

    Asura_PS2_VU1::LoadRenderer( Asura_PS2_VU1::RENDERER_DECAL );

    Asura_PS2_TextureManagement::SetBufferLayout( ASURA_PS2_TEXBFRLAYOUT_2_LARGE );

    // Initialise the renderer.
    {
        const u_int uNumCams      = Asura_Camera::GetNumberOfCameras();
        const u_int uNumCamQWs    = ASURA_MAX_NO_OF_CAMERAS * uPER_CAM_DATA_SIZE;
        const u_int uSizeOfUNPACK = uNumCamQWs + 2;
        const u_int uQWC          = uSizeOfUNPACK + 1;
        const u_int uTotalQWs     = 1 + uQWC;

        Asura_PS2_QW* const pxStart = Asura_PS2_DeferredRendering::GetVifBuffer( 1 + uQWC );
        Asura_PS2_QW* pxOut = pxStart;

        (reinterpret_cast<Asura_PS2_DmaTag*>(pxOut++))->Set( uQWC,
                                                             Asura_PS2_DmaTag::PCE_NOCHANGE,
                                                             Asura_PS2_DmaTag::ID_CNT,
                                                             false,
                                                             0,
                                                             false,
                                                             SCE_VIF1_SET_STCYCL(1, 1, false),
                                                             SCE_VIF1_SET_UNPACK(0, uSizeOfUNPACK, Asura_PS2_VU1::VIFPK_V4_32, false) );

        for( u_int uCam = 0; uCam < uNumCams; ++uCam )
        {
            Asura_Camera::SetCameraIndex( uCam );

            // Add the W->GBC matrix.
            {
                sceVu0FMATRIX& aafWorld2Clip = Asura_PS2_VU1::GetWorldToClipMatGuardband();
                const u_long128* puW2C = reinterpret_cast<const u_long128*>( &aafWorld2Clip[0][0] );

                const u_long128 u0 = *(puW2C + 0);
                const u_long128 u1 = *(puW2C + 1);
                const u_long128 u2 = *(puW2C + 2);
                const u_long128 u3 = *(puW2C + 3);

                (pxOut + 0)->u128 = u0;
                (pxOut + 1)->u128 = u1;
                (pxOut + 2)->u128 = u2;
                (pxOut + 3)->u128 = u3;

                pxOut += 4;
            }

            // Add the GBC->S scale and translation vectors.
            {
                sceVu0FVECTOR& afScale = Asura_PS2_VU1::GetGBClipToScrScaleVec( uCam );
                sceVu0FVECTOR& afTrans = Asura_PS2_VU1::GetGBClipToScrTransVec( uCam );

                const u_long128 u0 = reinterpret_cast<const u_long128&>( afScale );
                const u_long128 u1 = reinterpret_cast<const u_long128&>( afTrans );

                (pxOut + 0)->u128 = u0;
                (pxOut + 1)->u128 = u1;

                // Add a slight offset to the z scale so the decal don't
                // z fight.
                float fModifiedTransZ = afTrans[2] + 300.0f;
                (pxOut + 1)->f32s[2] = fModifiedTransZ;

                pxOut += 2;
            }

            // Add the SCISSOR and OFFSET settings.
            pxOut = Asura_PS2_GS::GetViewportSetupGifCmds( uCam, Asura_PS2_GS::CONTEXT_1, pxOut );
        }

        pxOut += (ASURA_MAX_NO_OF_CAMERAS - uNumCams) * uPER_CAM_DATA_SIZE;

        // Camera setup GIF tag.
        pxOut->u64s[0] = SCE_GIF_SET_TAG( 2, 1, 0, 0, SCE_GIF_PACKED, 1 );
        pxOut->u64s[1] = SCE_GIF_PACKED_AD;
        ++pxOut;

        // Drawing gif tag
        {
            const u_int uPRIM = SCE_GS_SET_PRIM( SCE_GS_PRIM_TRISTRIP,
                                                 1,
                                                 1,
                                                 0,
                                                 1,
                                                 0,
                                                 0,
                                                 0,
                                                 0 );

            pxOut->u64s[0] = SCE_GIF_SET_TAG( 0, 0, 1, uPRIM, SCE_GIF_PACKED, 3 );
            pxOut->u64s[1] = (SCE_GS_XYZ2 << 8) | (SCE_GS_RGBAQ << 4) | (SCE_GS_ST << 0);
            ++pxOut;
        }

        // Kick off the setup code and set the double buffer layout
        pxOut->u32s[0] = SCE_VIF1_SET_ITOP( uNumCams, false );
        pxOut->u32s[1] = SCE_VIF1_SET_MSCAL( 0, false );
        pxOut->u32s[2] = SCE_VIF1_SET_BASE( uINPUT_BFR_START, 0 );
        pxOut->u32s[3] = SCE_VIF1_SET_OFFSET( uINPUT_BFR_SIZE, 0 );
        ++pxOut;

        Asura_Assert( static_cast<u_int>(pxOut - pxStart) == uTotalQWs, "" );
    }

    DecalBufferList::Render();
}

//
// DecalBufferList.
//

DecalBufferList* DecalBufferList::s_pxAllLists = NULL;
u_int            DecalBufferList::s_uNumLists  = 0;

void DecalBufferList::CreateAll( const Asura_PS2_DecalSystem::BufferListSpec* pxSpecs, u_int uNumSpecs )
{
    Asura_PS2_QW* pxAllVIFData;

    // Allocate one great big buffer for DMA chains.
    {
        u_int uTotalBfrs = 0;
        for( u_int u = 0; u < uNumSpecs; ++u )
        {
            uTotalBfrs += pxSpecs[u].uNumBfrs;
        }

        u_int uTotalSize = uTotalBfrs * (uMAX_CHAIN_SIZE_QWS << 4);
		uTotalSize = (uTotalSize + 63) & ~63;
        pxAllVIFData = static_cast<Asura_PS2_QW*>( Asura_AllocateAlign(uTotalSize, ASURA_ALIGN_64_BYTE) );
    }

    s_pxAllLists = new DecalBufferList[uNumSpecs];
    s_uNumLists = uNumSpecs;
    
    for( u_int u = 0; u < uNumSpecs; ++u )
    {
        s_pxAllLists[u].Initialise( pxSpecs[u], pxAllVIFData );
        pxAllVIFData += pxSpecs[u].uNumBfrs * uMAX_CHAIN_SIZE_QWS;
    }
}

void DecalBufferList::DeleteAll( void )
{
    if( s_pxAllLists )
    {
        // The first list's DMA chain ptr is the start of the single buffer
        // we allocated in CreateAll() so deal with that.
        Asura_DeallocateAlign( s_pxAllLists[0].GetChainAddr(0) );

        delete[] s_pxAllLists;

        s_pxAllLists = NULL;
        s_uNumLists  = 0;
    }
}

DecalBufferList* DecalBufferList::FindList( int iTexID, u_int uDecalFlags )
{
    uDecalFlags &= uDECAL_FLAGS_WE_CARE_ABOUT;

    for( u_int u = 0; u < s_uNumLists; ++u )
    {
        DecalBufferList& xCurList = s_pxAllLists[u];
        if( xCurList.m_iTexID == iTexID && xCurList.m_uDecalFlags == uDecalFlags )
        {
            return &xCurList;
        }
    }

    return NULL;
}

DecalBufferList::~DecalBufferList( void )
{
#if ASURA_PS2_COLLECT_RENDER_STATS
    delete[] m_pucTriCounts;
#endif
    delete[] m_pusQWCs;

    // NB: Deleting the DMA chain data will be dealt with in DeleteAll();
}

void DecalBufferList::AppendToScratchBfr( const Asura_Decal_Vertex* pxStrip, u_int uNumVerts, const Asura_RGBA& xDiffuseRGBA )
{
    Asura_Assert( CanFitInScratchBfr(uNumVerts), "" );

    u_int uABGR;

    // Convert RGBA in advance.
    {
        uABGR  = Asura_Maths::FloatToInt( Asura_Maths::Min(xDiffuseRGBA.m_fA * 128.0f, 128.0f) );
        uABGR <<=8;
        uABGR |= Asura_Maths::FloatToInt( Asura_Maths::Min(xDiffuseRGBA.m_fB * 255.0f, 255.0f) );
        uABGR <<=8;
        uABGR |= Asura_Maths::FloatToInt( Asura_Maths::Min(xDiffuseRGBA.m_fG * 255.0f, 255.0f) );
        uABGR <<=8;
        uABGR |= Asura_Maths::FloatToInt( Asura_Maths::Min(xDiffuseRGBA.m_fR * 255.0f, 255.0f) );
    }

    u_int uOutIdx = m_uNumVertsInScratch;
    for( u_int u = 0; u < uNumVerts; ++u )
    {
        const Asura_Decal_Vertex& xInVtx = pxStrip[u];

        DecalVtxWithColour& xOutVtx = m_axScratchBfr[uOutIdx++];

        xOutVtx.fX   = xInVtx.m_xPosition.x;
        xOutVtx.fY   = xInVtx.m_xPosition.y;
        xOutVtx.fZ   = xInVtx.m_xPosition.z;
        xOutVtx.uADC = ( u < 2 ? 0x8000 : 0 );

        // Convert to 1:3:12 fixed point.
        xOutVtx.sS = static_cast<short>( Asura_Maths::FloatToInt_Near(xInVtx.m_fU * 4096.0f) );
        xOutVtx.sT = static_cast<short>( Asura_Maths::FloatToInt_Near(xInVtx.m_fV * 4096.0f) );

        xOutVtx.uABGR = uABGR;
    }

    m_uNumVertsInScratch = uOutIdx;

#if ASURA_PS2_COLLECT_RENDER_STATS
    m_uNumTrisInScratch += uNumVerts - 2;
#endif 
}

bool DecalBufferList::FlushScratchBfr( void )
{
    // Check if it's safe to switch to the spare chain yet.
    u_int uCurrentFrame = Asura_Timers::GetFrameNumber();
    if( uCurrentFrame < m_uFrameAtWhichSpareChainIsValid )
    {
        return false;
    }

    u_int uDestChain = m_uSpareDMAChain;

#if ASURA_PS2_COLLECT_RENDER_STATS
    m_pucTriCounts[uDestChain] = m_uNumTrisInScratch;
#endif

	Asura_Assert( m_uNumVertsInScratch >= 3, "" );
	Asura_Assert( m_uNumVertsInScratch < MAX_DECAL_VERTS, "" );

    m_pusQWCs[uDestChain] = CreateDMAChain( &m_axScratchBfr[0],
                                            m_uNumVertsInScratch,
                                            Asura_PS2_UncachedAcceleratedMem(GetChainAddr(uDestChain)) );

    ++uDestChain;
    if( uDestChain == m_uNumDMAChains )
    {
        uDestChain = 0;
    }
    m_uSpareDMAChain = uDestChain;

    // If spare has caught up with first then we need to move that on.
    if( uDestChain == m_uFirstDMAChain )
    {
        m_uFirstDMAChain = uDestChain + 1;
        if( m_uFirstDMAChain == m_uNumDMAChains )
        {
            m_uFirstDMAChain = 0;
        }
    }

    m_uNumVertsInScratch = 0;
#if ASURA_PS2_COLLECT_RENDER_STATS
    m_uNumTrisInScratch  = 0;
#endif

    // Since we won't now render using the buffer that's becoming spare this
    // frame and since  by next frame last frame's deferred rendering should
    // be long finished, it should be fine to use it then.
    m_uFrameAtWhichSpareChainIsValid = uCurrentFrame + 1;

    return true;
}

void DecalBufferList::Initialise( const Asura_PS2_DecalSystem::BufferListSpec& xSpec, Asura_PS2_QW* pxChainWorkspace )
{
    // Specs should only be including decal flags that we're interested in.
    Asura_Assert( !(xSpec.uDecalFlags & ~uDECAL_FLAGS_WE_CARE_ABOUT), "" );

    m_iTexID      = xSpec.iTexID;
    m_uDecalFlags = xSpec.uDecalFlags;

    m_uNumVertsInScratch = 0;
#if ASURA_PS2_COLLECT_RENDER_STATS
    m_uNumTrisInScratch  = 0;
#endif

    // Setup DMA chain data.
    {
        m_pxDMAChains   = pxChainWorkspace;
        m_uNumDMAChains = xSpec.uNumBfrs;
        m_pusQWCs       = new u_short[m_uNumDMAChains];
#if ASURA_PS2_COLLECT_RENDER_STATS
        m_pucTriCounts  = new u_char[m_uNumDMAChains];
#endif

        m_uFirstDMAChain                 = 0;
        m_uSpareDMAChain                 = 0;
        m_uFrameAtWhichSpareChainIsValid = 0;
    }
}

void DecalBufferList::Render( void )
{
#if ASURA_PS2_COLLECT_RENDER_STATS
    u_int uNumTrisRendered = 0;
#endif

    for( u_int uList = 0; uList < s_uNumLists; ++uList )
    {
        const DecalBufferList& xList = s_pxAllLists[uList];

        const u_int uFirstDMAChain   = xList.m_uFirstDMAChain;
        const u_int uSpareDMAChain   = xList.m_uSpareDMAChain;
        const u_int uNumScratchVerts = xList.m_uNumVertsInScratch;

        // Skip this chain if it has nothing to contribute.
        if( uFirstDMAChain == uSpareDMAChain && !uNumScratchVerts )
        {
            continue;
        }

        bool bTextureUploaded = Asura_PS2_TextureManagement::UploadTexture( xList.m_iTexID );
    
        // Note that we need to FLUSH after rendering a previous list since
        // we're about to send renderstate via a DIRECT below. Fortunately
        // SetupTextureEnvironment() will take care of that for us.
        Asura_PS2_TextureManagement::SetupTextureEnvironment( bTextureUploaded,
                                                              ASURA_PS2_TEXREGFLAG_ALL,
                                                              xList.m_iTexID,
                                                              Asura_PS2_TextureManagement::BFR_AUTO,
                                                              SCE_GS_LINEAR,
                                                              SCE_GS_LINEAR_MIPMAP_NEAREST );

        Asura_PlatformRenderStates::CreateDefaultRenderStatePacket( Asura_PlatformRenderStates::CONTEXT_1 );
        Asura_PlatformRenderStates::SetTextureClamp( true );
        Asura_PlatformRenderStates::SetZBufferWriteEnabled( false );
        if( xList.m_uDecalFlags & ASURA_DECAL_DETAILMAP )
        {
            Asura_PlatformRenderStates::SetAlphaBlend( SCE_GS_ALPHA_CD, SCE_GS_ALPHA_ZERO, SCE_GS_ALPHA_AS, SCE_GS_ALPHA_ZERO, 0 );
        }
        Asura_PlatformRenderStates::ApplyRenderStates();


        // Send cached chains.
        {
            const u_int uTotalChains = xList.m_uNumDMAChains;

            u_int uNumChainsToSend;
            if( uSpareDMAChain >= uFirstDMAChain )
            {
                uNumChainsToSend = uSpareDMAChain - uFirstDMAChain;
            }
            else
            {
                uNumChainsToSend = uSpareDMAChain + (uTotalChains - uFirstDMAChain);
            }

            Asura_PS2_DmaTag* pxTags = reinterpret_cast<Asura_PS2_DmaTag*>( Asura_PS2_DeferredRendering::GetVifBuffer(uNumChainsToSend) );

            for( u_int uBfr = uFirstDMAChain; uBfr != uSpareDMAChain; )
            {
                (pxTags++)->SetWithNOPs( xList.m_pusQWCs[uBfr],
                                         Asura_PS2_DmaTag::PCE_NOCHANGE, 
                                         Asura_PS2_DmaTag::ID_REF,
                                         false, 
                                         reinterpret_cast<u_int>(xList.GetChainAddr(uBfr)),
                                         false );

#if ASURA_PS2_COLLECT_RENDER_STATS
                uNumTrisRendered += xList.m_pucTriCounts[uBfr];
#endif

                ++uBfr;
                if( uBfr == uTotalChains )
                {
                    uBfr = 0;
                }
            }
        }

        // Send whatever we've got in the scratch buffer.
        if( uNumScratchVerts )
        {
            const u_int uQWC      = CALC_CHAIN_SIZE_QWS( uNumScratchVerts );
            const u_int uTotalQWs = 1 + uQWC;

            Asura_PS2_QW* pxOut = Asura_PS2_DeferredRendering::GetVifBuffer( uTotalQWs );

            (reinterpret_cast<Asura_PS2_DmaTag*>(pxOut++))->SetWithNOPs( uQWC,
                                                                         Asura_PS2_DmaTag::PCE_NOCHANGE, 
                                                                         Asura_PS2_DmaTag::ID_CNT,
                                                                         false, 
                                                                         0,
                                                                         false );

            CreateDMAChain( xList.m_axScratchBfr, uNumScratchVerts, pxOut );

#if ASURA_PS2_COLLECT_RENDER_STATS
            uNumTrisRendered += xList.m_uNumTrisInScratch;
#endif
        }
    }

    // FLUSH after final list, before we start rendering something else.
    {
        Asura_PS2_QW* pxOut = Asura_PS2_DeferredRendering::GetVifBuffer( 1 );

        (reinterpret_cast<Asura_PS2_DmaTag*>(pxOut))->Set( 0,
                                                           Asura_PS2_DmaTag::PCE_NOCHANGE, 
                                                           Asura_PS2_DmaTag::ID_CNT,
                                                           false, 
                                                           0,
                                                           false,
                                                           SCE_VIF1_SET_NOP(false),
                                                           SCE_VIF1_SET_FLUSH(false) );
    }

#if ASURA_PS2_COLLECT_RENDER_STATS
    Asura_PS2_Render::UpdateTriStats( uNumTrisRendered * Asura_Camera::GetNumberOfCameras(), 1 );
#endif
}

// S T A T I C   F U N C T I O N S ///////////////////////////////////////////

static u_int CreateDMAChain( const DecalVtxWithColour* pxDecalVerts, u_int uNumVerts, Asura_PS2_QW* pxOut )
{
	Asura_Assert( uNumVerts >= 3, "" );
	Asura_Assert( uNumVerts < MAX_DECAL_VERTS, "" );
    u_int uWord = 0;

    pxOut->u32s[uWord++] = SCE_VIF1_SET_STCYCL( 1, 3, false );

    // Positions.
    {
        pxOut->u32s[uWord++] = SCE_VIF1_SET_UNPACKR( 0, uNumVerts, Asura_PS2_VU1::VIFPK_V4_32, false );
        
        for( u_int uVtx = 0; uVtx < uNumVerts; ++uVtx )
        {
            const DecalVtxWithColour& xVtx = pxDecalVerts[uVtx];

            float fX   = xVtx.fX;
            float fY   = xVtx.fY;
            float fZ   = xVtx.fZ;
            u_int uADC = xVtx.uADC;

            pxOut->f32s[uWord++] = fX;
            pxOut->f32s[uWord++] = fY;
            pxOut->f32s[uWord++] = fZ;
            pxOut->u32s[uWord++] = uADC;
        }
    }

    // STs.
    {
        pxOut->u32s[uWord++] = SCE_VIF1_SET_UNPACKR( 1, uNumVerts, Asura_PS2_VU1::VIFPK_V2_16, false );

        uWord <<= 1;
        for( u_int uVtx = 0; uVtx < uNumVerts; ++uVtx )
        {
            const DecalVtxWithColour& xVtx = pxDecalVerts[uVtx];

            short sS = xVtx.sS;
            short sT = xVtx.sT;

            pxOut->i16s[uWord++] = sS;
            pxOut->i16s[uWord++] = sT;
        }
        uWord >>= 1;
    }

    // RGBAs.
    {
        pxOut->u32s[uWord++] = SCE_VIF1_SET_UNPACKRU( 2, uNumVerts, Asura_PS2_VU1::VIFPK_V4_8, false );

        for( u_int uVtx = 0; uVtx < uNumVerts; ++uVtx )
        {
            pxOut->u32s[uWord++] = pxDecalVerts[uVtx].uABGR;
        }
    }

    // Set ITOP and start it rendering.
    pxOut->u32s[uWord++] = SCE_VIF1_SET_ITOP( uNumVerts, false );
    pxOut->u32s[uWord++] = SCE_VIF1_SET_MSCNT( false );

    // Pad to QW alignment.
    u_int uRemainder = uWord & 3;
    if( uRemainder )
    {
        for( u_int u = 4 - uRemainder; u; --u )
        {
            pxOut->u32s[uWord++] = SCE_VIF1_SET_NOP( false );
        }
    }

    u_int uQWs = uWord >> 2;

    Asura_Assert( CALC_CHAIN_SIZE_QWS(uNumVerts) == uQWs, "" );
    Asura_Assert( uQWs <= uMAX_CHAIN_SIZE_QWS, "" );

    return uQWs;
}
