Files
TeslaRel410/sda4/DPL3/VRENDER/PXPL5SUP/GOODEQNS.C
T
CydandClaude Fable 5 db7745fcd0 sda4: commit the Glaze developer hard-drive dump
Un-ignored: the dev drive is the ground truth the restoration and
emulator work constantly reference (DPL3/LIBDPL + VRENDER i860 renderer
source, BT/RP live+dev game trees, VGL_LABS pod boot, scene/audio
content). Kept in-repo for the pod-owner community.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:41:15 -05:00

441 lines
13 KiB
C++

/*{{{ decls associated with DIVISION pxpl5 implementation*/
#define divpl5_xshift 6
#define divpl5_yshift 7
/*}}} */
/*{{{ on pxpl5 oddness*/
/*
pxpl5 forces you to do 3 things you normally wouldnt do on
a graphics system - edgeize, planarize and binitize primitives
edgeizing involves turning a polygon into a set of edges, each
edge described by an expression of the form f(x,y) = Ax + By + C,
where conventionally a point is INSIDE the edge if f(x,y) > 0 at (x,y)
planarization is pretty similar, and is used for Z-buffering,
Gouraud-shading and texturing. Planarization involves computing a
screen-space planar equation for a given variable - so to Z-buffer,
Gouraud-shade and texture a triangle we need to compute
Z=fz(x,y), r=fr(x,y), g=fg(x,y), b=fb(x,y), u=fu(x,y), v=fv(x,y)
where each of fz, fr, fg, fb, fu, fv are cast as expression of the form
f=Ax + By + C
binitization is different, and stems from the original MIMDness of pxpl5 -
rather than build a 640x512 array of pixel-processors, we use multiple
arrays of 128x128 (or 64x128) and if the polygons scatter statistically
well, we can get many times more performance for a given number of
pixel-processors
in order to do this, as a triangle is transformed to screen-space, we need
to determine how many screen-space regions of 64x128 are overlapped by the
triangle, and place the triangle into 'bins' associated with each region.
binitization may kill me yet.
*/
/*}}} */
/*{{{ typedefs for planarization*/
typedef struct s_bininfo {
int bin_minx;
int bin_miny;
int bin_maxx;
int bin_maxy;
} bininfo;
typedef struct s_preplane {
float x23;
float x31;
float x12;
float C;
} preplane;
/*}}} */
/*{{{ void edgeize ( float *eqn, float *p1, float *p2 )*/
void edgeize ( float *eqn, float *p1, float *p2 )
{
/* *********************
takes 2 point p1 and p2 and computes the edge
equation edge Ax + By + C, +ve inside, -ve outside
the edge
4 cases -
a) p0 b) p1
\ /
\ /
\ /
p1 p0
c) p1 d) p0
\ /
\ /
\ /
p0 p1
We need to ensure that in all cases we treat the edges identically,
e.g a = c with flipped vertices / opcode, ditto b, d
*/
float A, B, C;
/*
however, 1st approximation - this will suffer
rounding errors + DDA cracks
*/
eqn[0]=p1[Y] - p2[Y];
eqn[1]=p2[X] - p1[X];
eqn[2]=(p2[Y]*p1[X]) - (p2[X]*p1[Y]);
}
/*}}} */
/*{{{ on planarization*/
/*
Examination of the equations for planarization, and the UNC rendering
library, indicates some useful speedups for planarizing.
A recurring term is the divisor for all 3 coefficients, termed C. This
is independent of the planarized variable; it only varies with screen-space
X and Y, so can be precomputed once per triangle and re-used for
all planarized expressions
ditto some recurring difference expressions (x1 - x2 etc.)
So preplanarize precomputes the useful stuff into a structure for
subsequent planarizing. Ideally of course we precompute this into
a set of floating-point registers. Later.
Or maybe right now - how many registers do I need to do this?
rx23 \
rx31 > the recurring differences
rx12 /
rC the divisor for preplanarizing; now to planarize, strive to keep
rv1y
rv2y
rv3y
rv1x
rv2x and
rv3x in registers also
so we keep 10 fp registers hanging around, so to planarize a variable
we access memory 3 times, to load
v1
v2
v3 (which are used many times) using up just 13 fp registers
then 3 writes to eqn->A, eqn->B, eqn->C
this should be very fast indeed on an XP
The correct structure for the code is probably a dispatch vector of
C functions associated with each type of triangle - e.g 24-bit Gouraud,
8-bit Gouraud + intrinsic + spec
8-bit Gouraud + intrinsic + spec + texture + MIP etc,
the function calls an assembly stub which preplanarizes, then
repeatedly calls an assembly-coded planarize as many times as needed
Planarization looks like a cost of 25 ticks per planarized variable,
so we can planarize
Z, diffuse, spec in 1.5 uS (667k triangles/sec)
Z, diffuse, spec, u, v, homo in 3uS (333k )
Z, diffuse, spec, u, v, homo, MIP in 3.5uS, or (286k )
it looks like we can edgize in about 20 ticks per edge, or 1.2 uS per
triangle, so the edgize / planarize costs become
Z, diffuse, spec in 2.7 uS (370k triangles/sec)
Z, diffuse, spec, u, v, homo in 4.2 uS (238k )
Z, diffuse, spec, u, v, homo, MIP in 3.5uS, or (212k )
or the VWE benchmark of flat-shaded textured quads -
Z, u, v, homo, MIP in 3.6uS or (278k )
SO we are in shape performance-wise for planarization. How is binitizing.
Read on.
*/
/*}}} */
/*{{{ void preplanarize ( float *coeffs, preplane *p, float *v1, float *v2, float *v3 )*/
void preplanarize ( float *coeffs, preplane *p, float *v1, float *v2, float *v3 )
{
p->x23=v2[X] - v3[X];
p->x31=v3[X] - v1[X];
p->x12=v1[X] - v2[X];
p->C=1.0f / ((v1[X] * (v2[Y] - v3[Y])) +
(v2[X] * (v3[Y] - v1[Y])) +
(v3[X] * (v1[Y] - v2[Y])));
}
/*}}} */
/*{{{ void planarize ( float *eqn, float *v1, float *v2, float *v3, int index, preplane *p )*/
void planarize ( float *eqn, float *v1, float *v2, float *v3, int index, preplane *p )
{
eqn[0]=-p->C* ((v1[Y] * (v2[index] - v3[index])) +
(v2[Y] * (v3[index] - v1[index])) +
(v3[Y] * (v1[index] - v2[index])));
eqn[1]=-p->C*((v1[index] * p->x23) +
(v2[index] * p->x31) +
(v3[index] * p->x12));
eqn[2]= p->C*((v1[X]*((v2[Y]*v3[index]) - (v3[Y]*v2[index]))) +
(v2[X]*((v3[Y]*v1[index]) - (v1[Y]*v3[index]))) +
(v3[X]*((v1[Y]*v2[index]) - (v2[Y]*v1[index]))));
}
/*}}} */
/*{{{ binitizing*/
/*{{{ datastructures for binitizing*/
/* *********************************
what are the best datastructures for binitizing ?
they need working out in conjunction with the DMA engine
protocol
the DMA engine takes, per bin, an array of 64-bit words, organised as
address:count|opcode
the address is where the IGC data resides, the count is the no of
64-bit words in the IGC packet. The packet could be typically a
triangle, a partial triangle, a sphere or a chunk of end-of-frame data
triangles are 30-40 32-bit words long.
a screen may be anything from 512 to 1280 pixels wide, which is from 8
to 64 bins wide, typically 10 (NTSC) - awkward, needs * rather than <<
a typical bin data looks like
address:count
address:count
address:count
...
address:count
in contiguous memory locations. How to assemble these live with
minimal mallocing?
YEAH - good one hardware guys. The macro language supports a GOTO;
so we simply malloc chunks of say 32 64-bit words, and the last one
contains GOTO next chunk. So we never need to memcpy. In fact in the
steady-state we never need to malloc.
So if we have to render a triangle, we have to put its IGC data into
memory, then reference this data from multiple bin lists. All rendered
triangles can be put into a huge pool (double-buffered), which is
just incremented.
So we have the structures in place - how do we binitize?
Lets render a triangle -
pre --> enable voodoo 1
edge 4
edge 4
edge 4
z compare 4
z replace 4
lum 4
spec 4
scalar_stuff 1
p -->
We need to place pre:42 (pre:21?) into all the bins the triangle overlaps
For each bin we need a head chunk, and a tail chunk. Each chunk contains a
count (so I know where to put the next triangle).
I think I am starting to understand how to do this ...
*/
/*}}} */
/*{{{ typedefs / decls for binitization*/
#define BIN_FULL (63*2) /* when index==this, chain into next chunk */
typedef struct s_binchunk {
double DMA_opcodes[64]; /* force dbl-alignment, 64 triangles worth */
int usage; /* in 32-bit words, always dbl-bumped */
struct s_binchunk *next; /* only used for housekeeping */
} binchunk;
typedef struct s_screenbin {
binchunk *head;
binchunk *tail;
} screenbin;
binchunk *free_binchunks=NULL;
screenbin *screen0bins=NULL,
*screen1bins=NULL,
*screenbins =NULL;
int DMAscreen=0, writeScreen=1;
/*}}} */
/*{{{ void create_screenbins ( int screenx, int screeny )*/
void create_screenbins ( int screenx, int screeny )
{
int binsx=screenx >> divpl5_xshift;
int binsy=screeny >> divpl5_yshift;
screen0bins=(screenbin *) malloc (binsx*binsy*sizeof(screenbin));
screen1bins=(screenbin *) malloc (binsx*binsy*sizeof(screenbin));
screenbins=screen0bins;
}
/*}}} */
/*{{{ void liberate_screenbins ( screenbin *screenbins, int screenx, int screeny )*/
void liberate_screenbins ( screenbin *screenbins, int screenx, int screeny )
{
/*
take the whole screen and put it back onto the free list, EXCEPT for
1st chunk in each screen region
*/
int i, j;
for (i=0; i<screenx; i++ ) {
for (j=0; j<screeny; j++ ) {
binchunk *chunk=screenbins->head;
if (chunk == NULL) {
printf ("Error, initially empty region in liberate_screenbins\n" );
}
chunk->usage=0;
chunk=chunk->next;
while (chunk) {
binchunk *nxt=chunk->next;
chunk->next=free_binchunks;
chunk=nxt;
}
screenbins++;
}
}
}
/*}}} */
/*{{{ void grab_binchunks ( int grab_chunks )*/
void grab_binchunks ( int grab_chunks )
{
/* mallocs and inits an initial tranche of binchunks */
int i;
for (i=0; i<grab_chunks; i++ ) {
binchunk *bin,
*prev=free_binchunks;
bin=(binchunk *) malloc( sizeof (binchunk));
if (bin == NULL) {
printf ("Malloc failed in grab_binchunk\n" );
}
bin->usage=0;
bin->next=prev;
prev=bin;
}
}
/*}}} */
/*{{{ binchunk *next_binchunk ()*/
binchunk *next_binchunk ()
{
if (free_binchunks == NULL)
grab_binchunks();
free_binchunks=free_binchunks->next;
return free_binchunks;
}
/*}}} */
/*{{{ void binitize ( int macro_lo, int macro_hi,*/
void binitize ( int macro_lo, int macro_hi,
float fminx, float fminy,
float fmaxx, float fmaxy,
int screen_maxx, int screen_maxy,
int screen_bins_x )
{
/*
binitizes a primitive of known screen-space extents
the DMA engine macros associated with the primitive are held
in macro_lo, macro_hi - typically { SEND macro_address,size }
the screen-space extents are held in fminx .. fmaxy, and
the integer screen resolution is held in screen_maxx, screen_maxy,
with (optimization) the bin-count in the x-direction held in
screen_bins_x
To binitize, we first work out what is the lower left corner bin,
then outer loop in y, inner loop x, dropping the macro into all
encountered bins.
*/
int minx, miny,
maxx, maxy;
minx=(int) fminx;
miny=(int) fminy;
maxx=(int) fmaxx;
maxy=(int) fmaxy;
if (maxx < 0) return;
if (maxy < 0) return;
if (minx > screen_maxx) return;
if (miny > screen_maxy) return;
minx >>= divpl5_xshift;
miny >>= divpl5_yshift;
maxx >>= divpl5_xshift;
maxy >>= divpl5_yshift;
/*
so we have minimax xy in screen-space bin indices -
put the data into bins
*/
{
/* get 1st bin */
screenbin *top_left_bin=&screenbins[(miny*screen_bins_x) + minx];
screenbin *sbin=top_left_bin, *lbin=sbin;
screenbin *xbin=lbin;
register int x;
/* scan down all y bins */
while (y < maxy) {
/* scan across all x bins */
for (x=maxx; x; x-- ) {
/* add doubleword macro to bin */
binchunk *bin=sbin->tail;
register int usage=bin->usage;
if (bin->usage == BIN_FULL) {
binchunk *nextbin=*next_binchunk ();
bin->DMA_opcodes[usage++]=(int) nextbin;
bin->DMA_opcodes[usage++]=DMA_GOTO;
bin=nextbin;
sbin->tail=bin;
usage=0;
}
bin->DMA_opcodes[usage++]=macro_lo;
bin->DMA_opcodes[usage++]=macro_hi;
bin->usage=usage;
xbin++;
}
lbin+=screen_bins_x;
}
}
}
/*}}} */
/*}}} */