Files
LibretroAdmin 26bfe6920c pvr: a render draws the render passes its region array points to
Virtua Fighter 4 (NAOMI 2) draws its reflections from stage 6 on - the
harbour's water under the metal grid floor, and the like - by sending a
frame's scene, continuing the lists (TA_LIST_CONT) with the scene as the
water reflects it at another object list address, and then starting two
renders of that one frame: the first to the screen, with a region array
whose entries point at the first pass's object lists, and the second to
a 512 x 256 texture at 0xAAF000, with a region array pointing at the
second pass's lists. The floor is drawn with that texture.

Here a render took the whole frame out of the list of frames being
written and drew every pass of it: the screen got the reflected scene
drawn over it (the beams across the picture), and the render to the
texture that followed found no frame and was dropped. The texture was
never drawn, and the floor showed whatever video memory held there -
rainbow noise, in every renderer and with either TA processing, as
reported. (The reference renderer, fed the same frame, shows the same.)

Now:
- When a list is started or continued, where its object lists go
  (TA_OL_BASE) is noted for that render pass (tad_context::ol_base).
- ta_select_passes(), as a render is started: the passes of the frame
  whose object lists the region array's entries point to - each entry's
  pass is the one whose lists start nearest below its lists - and only
  those are parsed and drawn (rend.pass_first, pass_last), with the
  region settings of the entries in order. A frame of one pass, or whose
  entries do not come out as one pass after another, is drawn whole as
  it always was. A render of some of the passes is parsed on the CPU
  (ta_gpu_prepare() leaves it to the full parse).
- Such a frame is left in the list (rend.keep, drawn), so that the next
  render of it finds it, and is recycled when a later render takes all
  of it. It is not a frame being written: a save state does not have it,
  the render that finds it again ends when a frame that is not there
  does (as before), and a list started there begins a frame as one made
  anew does (tactx_Restart()). So a state saved between the two renders
  and loaded runs on as it did, byte for byte (HEADLESS_AHEAD on the
  stage: 120 frames, all the same).
- A frame being written that comes from a save state has its last
  pass's object list address from the register, and the passes before
  it are taken to be below that, so the first frame after a load is
  drawn right too.

The save state's layout is as it was; nothing is allocated, and no
shader changes. tactx_Pop() is replaced by tactx_Get()/tactx_Remove().
2026-10-23 21:11:36 +02:00

653 lines
17 KiB
C++

#include "ta.h"
#include "ta_ctx.h"
#include "hw/holly/holly_intc.h"
#include "lockfree.h"
/*
Threaded TA Implementation
Main thread -> ta data -> stored (tactx)
Render/TA thread -> ta data -> draw lists -> draw
*/
#if HOST_CPU == CPU_X86
#include <xmmintrin.h>
struct simd256_t
{
DECL_ALIGN(32) __m128 data[2];
};
#elif HOST_CPU == CPU_ARM && defined(__ARM_NEON__)
#include <arm_neon.h>
struct simd256_t
{
DECL_ALIGN(32) uint64x2_t data[2];
};
#else
struct simd256_t
{
DECL_ALIGN(32) u64 data[4];
};
#endif
/*
Partial TA parsing for in emu-side handling. Properly tracks 32/64 byte state, and
Calls a helper function every time something important (SOL/EOL, etc) happens
Uses a state machine, with 3 bits state and 8 bits (PT:OBJ[6:2]) input
*/
enum ta_state
{
// -> TAS_NS, TAS_PLV32, TAS_PLHV32, TAS_PLV64, TAS_PLHV64, TAS_MLV64
TAS_NS, //
// -> TAS_NS, TAS_PLV32, TAS_PLHV32, TAS_PLV64, TAS_PLHV64
TAS_PLV32, //polygon list PMV<?>, V32
// -> TAS_NS, TAS_PLV32, TAS_PLHV32, TAS_PLV64, TAS_PLHV64
TAS_PLV64, //polygon list PMV<?>, V64
// -> TAS_NS, TAS_MLV64, TAS_MLV64_H
TAS_MLV64, //mv list
// -> TAS_PLV32
TAS_PLHV32, //polygon list PMV<64> 2nd half -> V32
// -> TAS_PLV64
TAS_PLHV64, //polygon list PMV<64> 2nd half -> V64
// -> TAS_PLV64_H
TAS_PLV64_H, //polygon list V64 2nd half
// -> TAS_MLV64
TAS_MLV64_H //mv list, 64 bit half
};
/* state | PTEOS | OBJ -> next, proc*/
#define ta_cur_state (ta_fsm[2048])
u8 ta_fsm[2049]; //[2048] stores the current state
u32 ta_fsm_cl=7;
static void fill_fsm(ta_state st, s8 pt, s8 obj, ta_state next, u32 proc=0, u32 sz64=0)
{
unsigned i, j;
for (i=0;i<8;i++)
{
if (pt != -1) i=pt;
for (j=0;j<32;j++)
{
if (obj != -1)
j=obj;
ta_fsm[(st<<8)+(i<<5)+j]=next | proc*16 /*| sz64*32*/;
if (obj != -1)
break;
}
if (pt != -1)
break;
}
}
static void fill_fsm(void)
{
unsigned i;
//initialise to invalid
for (i=0;i<2048;i++)
ta_fsm[i]=(i>>8) | 0x80;
for (i=0;i<8;i++)
{
switch(i)
{
case ParamType_End_Of_List:
{
//End of list -> process it !
fill_fsm(TAS_NS, ParamType_End_Of_List,-1,TAS_NS,1);
fill_fsm(TAS_PLV32,ParamType_End_Of_List,-1,TAS_NS,1);
fill_fsm(TAS_PLV64,ParamType_End_Of_List,-1,TAS_NS,1);
fill_fsm(TAS_MLV64,ParamType_End_Of_List,-1,TAS_NS,1);
}
break;
case ParamType_User_Tile_Clip:
case ParamType_Object_List_Set:
{
//32B commands, no state change
fill_fsm(TAS_NS,i,-1,TAS_NS);
fill_fsm(TAS_PLV32,i,-1,TAS_PLV32);
fill_fsm(TAS_PLV64,i,-1,TAS_PLV64);
fill_fsm(TAS_MLV64,i,-1,TAS_MLV64);
}
break;
case 3:
case 6:
//invalid
break;
case ParamType_Polygon_or_Modifier_Volume:
{
//right .. its complicated alirte
for (int k=0;k<32;k++)
{
u32 uid = TaTypeLut::instance().table[k * 4];
u32 vt=uid & 0x7f;
bool v64 = vt == 5 || vt == 6 || vt == 11 || vt == 12 || vt == 13 || vt == 14;
bool p64 = uid >> 31;
ta_state nxt = p64 ? (v64 ? TAS_PLHV64 : TAS_PLHV32) :
(v64 ? TAS_PLV64 : TAS_PLV32 ) ;
fill_fsm(TAS_PLV32,i,k,nxt,0,p64);
fill_fsm(TAS_PLV64,i,k,nxt,0,p64);
}
//32B command, no state change
fill_fsm(TAS_MLV64,i,-1,TAS_MLV64);
//process and start list
fill_fsm(TAS_NS,i,-1,TAS_NS,1);
}
break;
case ParamType_Sprite:
{
//SPR: 32B -> expect 64B data (PL*)
fill_fsm(TAS_PLV32,i,-1,TAS_PLV64);
fill_fsm(TAS_PLV64,i,-1,TAS_PLV64);
//invalid for ML
//process and start list
fill_fsm(TAS_NS,i,-1,TAS_NS,1);
}
break;
case ParamType_Vertex_Parameter:
{
//VTX: 32 B -> Expect more of it
fill_fsm(TAS_PLV32,i,-1,TAS_PLV32,0,0);
//VTX: 64 B -> Expect next 32B
fill_fsm(TAS_PLV64,i,-1,TAS_PLV64_H,0,1);
//MVO: 64B -> expect next 32B
fill_fsm(TAS_MLV64,i,-1,TAS_MLV64_H,0,1);
//invalid for NS
}
break;
}
}
//?
fill_fsm(TAS_PLHV32,-1,-1,TAS_PLV32); //64 PH -> expect V32
fill_fsm(TAS_PLHV64,-1,-1,TAS_PLV64); //64 PH -> expect V64
fill_fsm(TAS_PLV64_H,-1,-1,TAS_PLV64); //64 VH -> expect V64
fill_fsm(TAS_MLV64_H,-1,-1,TAS_MLV64); //64 MH -> expect M64
}
/* The control-word walk (ta_set_walk()). While it is on, every entry of
* ta_fsm for a block that is not a vertex - any parameter but a vertex,
* in a state that does not expect the second half of one - has this bit
* as well: the block is then handed to ta_handle_cmd() by the code that
* steps the state, which notes where the block is in the frame's table
* of them (TA_context::walk) and goes on as without it. Vertices, and
* everything while the walk is off, take the very same way as ever: the
* table holds no such bit then, and nothing else is looked at. */
#define TA_FSM_WALK 0x40
/* Whether ta_fsm has the bit (the emulation thread's), and whether it is
* wanted (from any thread: it is put in or taken out at the next list
* init, on the emulation thread, between two blocks) */
static bool ta_walk_on;
static retro_atomic_int_t ta_walk_wanted = RETRO_ATOMIC_INT_INITIALIZER(0);
/* The bit put in or taken out, the table of every frame being written
* forgotten: what has been sent to them so far was not walked, or walked
* without what follows */
static void ta_walk_apply(bool on)
{
u32 i;
for (i = 0; i < 2048; i++)
{
u8 e = ta_fsm[i] & ~TA_FSM_WALK;
if (on && (i >> 8) <= TAS_MLV64 && ((i >> 5) & 7) != ParamType_Vertex_Parameter)
e |= TA_FSM_WALK;
ta_fsm[i] = e;
}
ta_walk_on = on;
tactx_walk_forget();
}
void ta_set_walk(bool on)
{
retro_atomic_store_release_int(&ta_walk_wanted, on ? 1 : 0);
}
/* The block before ta_tad.thd_data goes in the frame's table: one store.
* A table that is full is not to be trusted for the rest of the frame,
* which is then parsed as it always is. */
static NOINLINE u32 ta_walk_note(u32 trans)
{
TA_context *ctx = ta_ctx;
u32 n = ctx->walk_count;
if (ctx->walk_restart)
{
ctx->walk_restart = false;
n = 0;
}
if (n < ctx->walk_size)
ctx->walk[n++] = (u32)(ta_tad.thd_data - 32 - ta_tad.thd_root) / 32;
else
n = TA_WALK_INVALID;
ctx->walk_count = n;
return trans & ~TA_FSM_WALK;
}
static const HollyInterruptID ListEndInterrupt[5]=
{
holly_OPAQUE,
holly_OPAQUEMOD,
holly_TRANS,
holly_TRANSMOD,
holly_PUNCHTHRU
};
static NOINLINE void DYNACALL ta_handle_cmd(u32 trans)
{
Ta_Dma* dat=(Ta_Dma*)(ta_tad.thd_data-32);
if (trans & TA_FSM_WALK)
{
trans = ta_walk_note(trans);
if ((trans & 0xF0) == 0)
{
// nothing else to do for it
ta_cur_state = (ta_state)trans;
return;
}
}
u32 cmd = trans>>4;
trans&=7;
//printf("Process state transition: %d || %d -> %d \n",cmd,state_in,trans&0xF);
if (cmd != 8)
{
if (dat->pcw.ParaType == ParamType_End_Of_List)
{
if (ta_fsm_cl==7)
ta_fsm_cl=dat->pcw.ListType;
//printf("List %d ended\n",ta_fsm_cl);
/* There are five lists and an interrupt for each. A list type of
* 5 to 7 is none of them: the list ends and nothing is raised. */
if (ta_fsm_cl < 5)
asic_RaiseInterruptBothCLX(ListEndInterrupt[ta_fsm_cl]);
ta_fsm_cl=7;
trans=TAS_NS;
}
else if (dat->pcw.ParaType == ParamType_Polygon_or_Modifier_Volume)
{
if (ta_fsm_cl==7)
ta_fsm_cl=dat->pcw.ListType;
if (!IsModVolList(ta_fsm_cl))
trans=TAS_PLV32;
else
trans=TAS_MLV64;
}
else if (dat->pcw.ParaType == ParamType_Sprite)
{
if (ta_fsm_cl==7)
ta_fsm_cl=dat->pcw.ListType;
//verify(!IsModVolList(ta_fsm_cl)); // fails with "F1 World Grand Prix for Dreamcast" and only with dynarec...
trans=TAS_PLV32;
}
}
u32 state_in = (trans<<8) | (dat->pcw.ParaType<<5) | (dat->pcw.obj_ctrl>>2)%32;
ta_cur_state = (ta_state)(ta_fsm[state_in]&0xF);
verify(ta_cur_state<=7);
}
static OnLoad ol_fillfsm(&fill_fsm);
void ta_vtx_ListCont(void)
{
SetCurrentTARC(TA_CURRENT_CTX);
ta_tad.Continue();
// (where this pass's object lists go: see ta_select_passes())
ta_tad.ol_base[ta_tad.render_pass_count] = TA_OL_BASE & 0xFFFFE0u;
ta_cur_state=TAS_NS;
ta_fsm_cl = 7;
}
void ta_vtx_ListInit(void)
{
if ((retro_atomic_load_relaxed_int(&ta_walk_wanted) != 0) != ta_walk_on)
ta_walk_apply(!ta_walk_on);
SetCurrentTARC(TA_CURRENT_CTX);
if (ta_ctx && ta_ctx->drawn)
tactx_Restart(ta_ctx);
ta_tad.ClearPartial();
ta_tad.ol_base[0] = TA_OL_BASE & 0xFFFFE0u;
/* (the data from here on is a new frame's: the table of the old one is
* kept for as long as it is what a render draws) */
if (ta_ctx)
ta_ctx->walk_restart = true;
ta_cur_state=TAS_NS;
ta_fsm_cl = 7;
}
void ta_vtx_SoftReset(void)
{
ta_cur_state=TAS_NS;
}
// The machine is reset: no list is open and no parameter is half sent
void ta_vtx_Reset(void)
{
ta_cur_state = TAS_NS;
ta_fsm_cl = 7;
}
int ta_vtx_list(void)
{
return ta_fsm_cl == 7 ? -1 : (int)ta_fsm_cl;
}
bool ta_vtx_half(void)
{
return ta_cur_state >= TAS_PLHV32;
}
/* A block the careful way: with no list begun, at the very start of a
* frame's data, or with the buffer full. */
static NOINLINE
void DYNACALL ta_data32_slow(void* data)
{
if (ta_ctx == NULL)
{
INFO_LOG(PVR, "Warning: data sent to TA prior to ListInit. Ignored");
return;
}
/* Where the next block goes. (Not End(): after the lists are started
* again the next block goes to the start of the buffer, whatever the
* frame before left in it, as the hardware's write pointer goes back to
* the base.) */
if ((u32)(ta_tad.thd_data - ta_tad.thd_root) >= ta_ctx->data_size)
{
INFO_LOG(PVR, "Warning: TA data buffer overflow");
asic_RaiseInterrupt(holly_MATR_NOMEM);
return;
}
simd256_t *dst = (simd256_t*)ta_tad.thd_data;
simd256_t *src = (simd256_t*)data;
/* The first block since the lists were started again over data that
* was never drawn: the places where lists were continued are places in
* that data, and not in this. (Left, the new data was cut into passes
* at them, and the first pass ran on into what was left of the old.) */
if (ta_tad.thd_data == ta_tad.thd_root && ta_tad.thd_old_data != ta_tad.thd_root)
ta_tad.render_pass_count = 0;
// First byte is PCW
PCW pcw = *(PCW*)data;
/* Copy the TA data */
if (dst != src)
*dst = *src;
ta_tad.thd_data+=32;
/* Process TA state */
u32 state_in = (ta_cur_state<<8) | (pcw.ParaType<<5) | (pcw.obj_ctrl>>2)%32;
u32 trans = ta_fsm[state_in];
ta_cur_state = (ta_state)trans;
bool must_handle = trans& 0xF0;
if (must_handle)
ta_handle_cmd(trans);
}
/* A block from a store queue. With data already in the buffer and room
* for more - one comparison says both - it is copied and the state moved
* on here; anything else is ta_data32_slow()'s. */
void DYNACALL ta_vtx_data32(void* data)
{
TA_context *ctx = ta_ctx;
u8 *to = ta_tad.thd_data;
if (ctx == NULL || (u32)(to - ta_tad.thd_root) - 32 >= ctx->data_size - 32)
{
ta_data32_slow(data);
return;
}
PCW pcw = *(PCW*)data;
*(simd256_t*)to = *(simd256_t*)data;
ta_tad.thd_data = to + 32;
u32 trans = ta_fsm[(ta_cur_state<<8) | (pcw.ParaType<<5) | (pcw.obj_ctrl>>2)%32];
ta_cur_state = (ta_state)trans;
if (trans & 0xF0)
ta_handle_cmd(trans);
}
/* Where the next block will be put, if @blocks more can be: something that
* makes blocks can make them there, and they are then taken by
* ta_vtx_data() without being copied. NULL: they cannot, or not there. */
u8 *ta_vtx_room(u32 blocks)
{
TA_context *ctx = ta_ctx;
u32 used;
if (ctx == NULL)
return NULL;
used = (u32)(ta_tad.thd_data - ta_tad.thd_root);
if (used > ctx->data_size || blocks > (ctx->data_size - used) / 32)
return NULL;
return ta_tad.thd_data;
}
/* The state machine's step for a block that has to be handled, as
* ta_handle_cmd() takes it but with nothing raised: for walking data
* again. @cl is the list that is open (ta_fsm_cl). */
static u32 ta_walk_cmd(u32 trans, PCW pcw, u32 *cl)
{
u32 cmd = trans >> 4;
trans &= 7;
if (cmd != 8)
{
if (pcw.ParaType == ParamType_End_Of_List)
{
*cl = 7;
trans = TAS_NS;
}
else if (pcw.ParaType == ParamType_Polygon_or_Modifier_Volume)
{
if (*cl == 7)
*cl = pcw.ListType;
trans = IsModVolList(*cl) ? TAS_MLV64 : TAS_PLV32;
}
else if (pcw.ParaType == ParamType_Sprite)
{
if (*cl == 7)
*cl = pcw.ListType;
trans = TAS_PLV32;
}
}
return ta_fsm[(trans<<8) | (pcw.ParaType<<5) | (pcw.obj_ctrl>>2)%32] & 0xF;
}
/* A frame's data that did not come in through here - a state was loaded -
* walked again, for its table: from the start, with the state machine
* starting over where the lists were started and continued. (The data
* goes as far as it was sent; what the tile accelerator was in the
* middle of is in the state, and the table goes on from there as the
* next blocks come.) Only while the walk is on. */
void ta_walk_rebuild(TA_context *ctx)
{
const tad_context &tad = ctx->tad;
const u8 *p, *end;
u32 n = 0, state = TAS_NS, cl = 7, pass = 0;
ctx->walk_count = TA_WALK_INVALID;
ctx->walk_restart = false;
if (!ta_walk_on)
return;
end = tad.End();
for (p = tad.thd_root; p < end; p += 32)
{
PCW pcw;
u32 trans;
while (pass < tad.render_pass_count && p >= tad.render_passes[pass])
{
// ta_vtx_ListCont()
state = TAS_NS;
cl = 7;
pass++;
}
pcw = *(const PCW *)p;
trans = ta_fsm[(state<<8) | (pcw.ParaType<<5) | (pcw.obj_ctrl>>2)%32];
if (trans & TA_FSM_WALK)
{
if (n == ctx->walk_size)
return;
ctx->walk[n++] = (u32)(p - tad.thd_root) / 32;
trans &= ~TA_FSM_WALK;
}
state = (trans & 0xF0) ? ta_walk_cmd(trans, pcw, &cl) : trans;
}
ctx->walk_count = n;
ctx->walk_restart = tad.thd_data == tad.thd_root;
}
/* Many blocks at once: a DMA transfer, or the NAOMI 2's geometry processor.
*
* Where to put the next one and the state are kept in registers across a
* run of them, and a run of vertices in a 32-byte format - most of what
* there ever is - changes neither the state nor anything else, so it is
* found by looking at one byte of each. Blocks made in the buffer itself
* (ta_vtx_room()) are not copied at all. */
void ta_vtx_data(u32* data, u32 size)
{
while (size > 0)
{
TA_context *ctx = ta_ctx;
u8 *to = ta_tad.thd_data;
u8 *root = ta_tad.thd_root;
u32 used = (u32)(to - root);
u32 count;
u32 state;
const u8 *from, *stop;
bool handled = false;
if (ctx == NULL || used >= ctx->data_size)
{
ta_data32_slow(data);
data += 8;
size--;
continue;
}
// (as in ta_data32_slow())
if (used == 0 && ta_tad.thd_old_data != root)
ta_tad.render_pass_count = 0;
count = (ctx->data_size - used) / 32;
if (count > size)
count = size;
state = ta_cur_state;
from = (const u8 *)data;
stop = from + count * 32;
if (from == to)
{
/* made in place */
while (from != stop)
{
PCW pcw;
u32 trans;
if (state == TAS_PLV32)
{
while (from != stop && ((const PCW *)from)->ParaType == ParamType_Vertex_Parameter)
from += 32;
if (from == stop)
break;
}
pcw = *(const PCW *)from;
trans = ta_fsm[(state<<8) | (pcw.ParaType<<5) | (pcw.obj_ctrl>>2)%32];
from += 32;
state = trans;
if (trans & 0xF0)
{
handled = true;
break;
}
}
to = (u8 *)from;
}
else
{
while (from != stop)
{
PCW pcw;
u32 trans;
if (state == TAS_PLV32)
{
while (from != stop && ((const PCW *)from)->ParaType == ParamType_Vertex_Parameter)
{
*(simd256_t *)to = *(const simd256_t *)from;
to += 32;
from += 32;
}
if (from == stop)
break;
}
pcw = *(const PCW *)from;
trans = ta_fsm[(state<<8) | (pcw.ParaType<<5) | (pcw.obj_ctrl>>2)%32];
*(simd256_t *)to = *(const simd256_t *)from;
to += 32;
from += 32;
state = trans;
if (trans & 0xF0)
{
handled = true;
break;
}
}
}
ta_tad.thd_data = to;
ta_cur_state = (ta_state)state;
if (handled)
ta_handle_cmd(state);
count = (u32)(from - (const u8 *)data) / 32;
data += count * 8;
size -= count;
}
}