//
// Copyright(C) 1993-1996 Id Software, Inc.
// Copyright(C) 2005-2014 Simon Howard
//
// 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
// of the License, 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.
//
// DESCRIPTION:
// Rendering main loop and setup functions,
// utility functions (BSP, geometry, trigonometry).
// See tables.c, too.
//
#include <stdlib.h>
#include <math.h>
#include "doomdef.h"
#include "doomstat.h"
#include "d_loop.h"
#include "m_bbox.h"
#include "m_menu.h"
#include "v_video.h"
#include "r_local.h"
#include "r_sky.h"
#include "i_webinput.h"
#include "st_stuff.h"
// Fineangles in the SCREENWIDTH wide window.
#define FIELDOFVIEW 2048
int viewangleoffset;
// increment every time a check is made
int validcount = 1;
lighttable_t* fixedcolormap;
int centerx;
int centery;
fixed_t centerxfrac;
fixed_t centeryfrac;
fixed_t projection;
// just for profiling purposes
int framecount;
int sscount;
int linecount;
int loopcount;
fixed_t viewx;
fixed_t viewy;
fixed_t viewz;
angle_t viewangle;
fixed_t viewcos;
fixed_t viewsin;
player_t* viewplayer;
// 0 = high, 1 = low
int detailshift;
//
// precalculated math tables
//
angle_t clipangle;
// The viewangletox[viewangle + FINEANGLES/4] lookup
// maps the visible view angles to screen X coordinates,
// flattening the arc to a flat projection plane.
// There will be many angles mapped to the same X.
int viewangletox[FINEANGLES/2];
// The xtoviewangleangle[] table maps a screen pixel
// to the lowest viewangle that maps back to x ranges
// from clipangle to -clipangle.
angle_t xtoviewangle[SCREENWIDTH+1];
lighttable_t* scalelight[LIGHTLEVELS][MAXLIGHTSCALE];
lighttable_t* scalelightfixed[MAXLIGHTSCALE];
lighttable_t* zlight[LIGHTLEVELS][MAXLIGHTZ];
// bumped light from gun blasts
int extralight;
void (*colfunc) (void);
void (*basecolfunc) (void);
void (*fuzzcolfunc) (void);
void (*transcolfunc) (void);
void (*spanfunc) (void);
//
// R_AddPointToBox
// Expand a given bbox
// so that it encloses a given point.
//
void
R_AddPointToBox
( int x,
int y,
fixed_t* box )
{
if (x< box[BOXLEFT])
box[BOXLEFT] = x;
if (x> box[BOXRIGHT])
box[BOXRIGHT] = x;
if (y< box[BOXBOTTOM])
box[BOXBOTTOM] = y;
if (y> box[BOXTOP])
box[BOXTOP] = y;
}
//
// R_PointOnSide
// Traverse BSP (sub) tree,
// check point against partition plane.
// Returns side 0 (front) or 1 (back).
//
int
R_PointOnSide
( fixed_t x,
fixed_t y,
node_t* node )
{
fixed_t dx;
fixed_t dy;
fixed_t left;
fixed_t right;
if (!node->dx)
{
if (x <= node->x)
return node->dy > 0;
return node->dy < 0;
}
if (!node->dy)
{
if (y <= node->y)
return node->dx < 0;
return node->dx > 0;
}
dx = (x - node->x);
dy = (y - node->y);
// Try to quickly decide by looking at sign bits.
if ( (node->dy ^ node->dx ^ dx ^ dy)&0x80000000 )
{
if ( (node->dy ^ dx) & 0x80000000 )
{
// (left is negative)
return 1;
}
return 0;
}
left = FixedMul ( node->dy>>FRACBITS , dx );
right = FixedMul ( dy , node->dx>>FRACBITS );
if (right < left)
{
// front side
return 0;
}
// back side
return 1;
}
int
R_PointOnSegSide
( fixed_t x,
fixed_t y,
seg_t* line )
{
fixed_t lx;
fixed_t ly;
fixed_t ldx;
fixed_t ldy;
fixed_t dx;
fixed_t dy;
fixed_t left;
fixed_t right;
lx = line->v1->x;
ly = line->v1->y;
ldx = line->v2->x - lx;
ldy = line->v2->y - ly;
if (!ldx)
{
if (x <= lx)
return ldy > 0;
return ldy < 0;
}
if (!ldy)
{
if (y <= ly)
return ldx < 0;
return ldx > 0;
}
dx = (x - lx);
dy = (y - ly);
// Try to quickly decide by looking at sign bits.
if ( (ldy ^ ldx ^ dx ^ dy)&0x80000000 )
{
if ( (ldy ^ dx) & 0x80000000 )
{
// (left is negative)
return 1;
}
return 0;
}
left = FixedMul ( ldy>>FRACBITS , dx );
right = FixedMul ( dy , ldx>>FRACBITS );
if (right < left)
{
// front side
return 0;
}
// back side
return 1;
}
//
// R_PointToAngle
// To get a global angle from cartesian coordinates,
// the coordinates are flipped until they are in
// the first octant of the coordinate system, then
// the y (<=x) is scaled and divided by x to get a
// tangent (slope) value which is looked up in the
// tantoangle[] table.
//
angle_t
R_PointToAngle
( fixed_t x,
fixed_t y )
{
x -= viewx;
y -= viewy;
if ( (!x) && (!y) )
return 0;
if (x>= 0)
{
// x >=0
if (y>= 0)
{
// y>= 0
if (x>y)
{
// octant 0
return tantoangle[ SlopeDiv(y,x)];
}
else
{
// octant 1
return ANG90-1-tantoangle[ SlopeDiv(x,y)];
}
}
else
{
// y<0
y = -y;
if (x>y)
{
// octant 8
return -tantoangle[SlopeDiv(y,x)];
}
else
{
// octant 7
return ANG270+tantoangle[ SlopeDiv(x,y)];
}
}
}
else
{
// x<0
x = -x;
if (y>= 0)
{
// y>= 0
if (x>y)
{
// octant 3
return ANG180-1-tantoangle[ SlopeDiv(y,x)];
}
else
{
// octant 2
return ANG90+ tantoangle[ SlopeDiv(x,y)];
}
}
else
{
// y<0
y = -y;
if (x>y)
{
// octant 4
return ANG180+tantoangle[ SlopeDiv(y,x)];
}
else
{
// octant 5
return ANG270-1-tantoangle[ SlopeDiv(x,y)];
}
}
}
return 0;
}
angle_t
R_PointToAngle2
( fixed_t x1,
fixed_t y1,
fixed_t x2,
fixed_t y2 )
{
viewx = x1;
viewy = y1;
return R_PointToAngle (x2, y2);
}
fixed_t
R_PointToDist
( fixed_t x,
fixed_t y )
{
int angle;
fixed_t dx;
fixed_t dy;
fixed_t temp;
fixed_t dist;
fixed_t frac;
dx = abs(x - viewx);
dy = abs(y - viewy);
if (dy>dx)
{
temp = dx;
dx = dy;
dy = temp;
}
// Fix crashes in udm1.wad
if (dx != 0)
{
frac = FixedDiv(dy, dx);
}
else
{
frac = 0;
}
angle = (tantoangle[frac>>DBITS]+ANG90) >> ANGLETOFINESHIFT;
// use as cosine
dist = FixedDiv (dx, finesine[angle] );
return dist;
}
//
// R_InitPointToAngle
//
void R_InitPointToAngle (void)
{
// UNUSED - now getting from tables.c
#if 0
int i;
long t;
float f;
//
// slope (tangent) to angle lookup
//
for (i=0 ; i<=SLOPERANGE ; i++)
{
f = atan( (float)i/SLOPERANGE )/(3.141592657*2);
t = 0xffffffff*f;
tantoangle[i] = t;
}
#endif
}
//
// R_ScaleFromGlobalAngle
// Returns the texture mapping scale
// for the current line (horizontal span)
// at the given angle.
// rw_distance must be calculated first.
//
fixed_t R_ScaleFromGlobalAngle (angle_t visangle)
{
fixed_t scale;
angle_t anglea;
angle_t angleb;
int sinea;
int sineb;
fixed_t num;
int den;
// UNUSED
#if 0
{
fixed_t dist;
fixed_t z;
fixed_t sinv;
fixed_t cosv;
sinv = finesine[(visangle-rw_normalangle)>>ANGLETOFINESHIFT];
dist = FixedDiv (rw_distance, sinv);
cosv = finecosine[(viewangle-visangle)>>ANGLETOFINESHIFT];
z = abs(FixedMul (dist, cosv));
scale = FixedDiv(projection, z);
return scale;
}
#endif
anglea = ANG90 + (visangle-viewangle);
angleb = ANG90 + (visangle-rw_normalangle);
// both sines are allways positive
sinea = finesine[anglea>>ANGLETOFINESHIFT];
sineb = finesine[angleb>>ANGLETOFINESHIFT];
num = FixedMul(projection,sineb)<<detailshift;
den = FixedMul(rw_distance,sinea);
if (den > num>>FRACBITS)
{
scale = FixedDiv (num, den);
if (scale > 64*FRACUNIT)
scale = 64*FRACUNIT;
else if (scale < 256)
scale = 256;
}
else
scale = 64*FRACUNIT;
return scale;
}
//
// R_InitTables
//
void R_InitTables (void)
{
// UNUSED: now getting from tables.c
#if 0
int i;
float a;
float fv;
int t;
// viewangle tangent table
for (i=0 ; i<FINEANGLES/2 ; i++)
{
a = (i-FINEANGLES/4+0.5)*PI*2/FINEANGLES;
fv = FRACUNIT*tan (a);
t = fv;
finetangent[i] = t;
}
// finesine table
for (i=0 ; i<5*FINEANGLES/4 ; i++)
{
// OPTIMIZE: mirror...
a = (i+0.5)*PI*2/FINEANGLES;
t = FRACUNIT*sin (a);
finesine[i] = t;
}
#endif
}
//
// R_InitTextureMapping
//
void R_InitTextureMapping (void)
{
int i;
int x;
int t;
fixed_t focallength;
// Use tangent table to generate viewangletox:
// viewangletox will give the next greatest x
// after the view angle.
//
// Calc focallength
// so FIELDOFVIEW angles covers SCREENWIDTH.
focallength = FixedDiv (centerxfrac,
finetangent[FINEANGLES/4+FIELDOFVIEW/2] );
for (i=0 ; i<FINEANGLES/2 ; i++)
{
if (finetangent[i] > FRACUNIT*2)
t = -1;
else if (finetangent[i] < -FRACUNIT*2)
t = viewwidth+1;
else
{
t = FixedMul (finetangent[i], focallength);
t = (centerxfrac - t+FRACUNIT-1)>>FRACBITS;
if (t < -1)
t = -1;
else if (t>viewwidth+1)
t = viewwidth+1;
}
viewangletox[i] = t;
}
// Scan viewangletox[] to generate xtoviewangle[]:
// xtoviewangle will give the smallest view angle
// that maps to x.
for (x=0;x<=viewwidth;x++)
{
i = 0;
while (viewangletox[i]>x)
i++;
xtoviewangle[x] = (i<<ANGLETOFINESHIFT)-ANG90;
}
// Take out the fencepost cases from viewangletox.
for (i=0 ; i<FINEANGLES/2 ; i++)
{
t = FixedMul (finetangent[i], focallength);
t = centerx - t;
if (viewangletox[i] == -1)
viewangletox[i] = 0;
else if (viewangletox[i] == viewwidth+1)
viewangletox[i] = viewwidth;
}
clipangle = xtoviewangle[0];
}
//
// R_InitLightTables
// Only inits the zlight table,
// because the scalelight table changes with view size.
//
#define DISTMAP 2
void R_InitLightTables (void)
{
int i;
int j;
int level;
int startmap;
int scale;
// Calculate the light levels to use
// for each level / distance combination.
for (i=0 ; i< LIGHTLEVELS ; i++)
{
startmap = ((LIGHTLEVELS-1-i)*2)*NUMCOLORMAPS/LIGHTLEVELS;
for (j=0 ; j<MAXLIGHTZ ; j++)
{
// Relative to ORIGWIDTH, not SCREENWIDTH - see pspritescale above.
scale = FixedDiv ((ORIGWIDTH/2*FRACUNIT), (j+1)<<LIGHTZSHIFT);
scale >>= LIGHTSCALESHIFT;
level = startmap - scale/DISTMAP;
if (level < 0)
level = 0;
if (level >= NUMCOLORMAPS)
level = NUMCOLORMAPS-1;
zlight[i][j] = colormaps + level*256;
}
}
}
//
// R_SetViewSize
// Do not really change anything here,
// because it might be in the middle of a refresh.
// The change will take effect next refresh.
//
boolean setsizeneeded;
int setblocks;
int setdetail;
void
R_SetViewSize
( int blocks,
int detail )
{
setsizeneeded = true;
setblocks = blocks;
setdetail = detail;
}
//
// R_SetupViewSizeTables
// Shared core of R_ExecuteSetViewSize / R_SetSplitscreenViewSize: rebuilds
// every table that depends on the current scaledviewwidth/viewheight/
// detailshift. Callers are responsible for setting those three globals
// first - this only differs from the historical R_ExecuteSetViewSize body
// in being callable with view dimensions that didn't come from the
// setblocks status-bar-size setting (i.e. splitscreen tiles).
//
static void R_SetupViewSizeTables (void)
{
fixed_t cosadj;
fixed_t dy;
int i;
int j;
int level;
int startmap;
viewwidth = scaledviewwidth>>detailshift;
centery = viewheight/2;
centerx = viewwidth/2;
centerxfrac = centerx<<FRACBITS;
centeryfrac = centery<<FRACBITS;
projection = centerxfrac;
if (!detailshift)
{
colfunc = basecolfunc = R_DrawColumn;
fuzzcolfunc = R_DrawFuzzColumn;
transcolfunc = R_DrawTranslatedColumn;
spanfunc = R_DrawSpan;
}
else
{
colfunc = basecolfunc = R_DrawColumnLow;
fuzzcolfunc = R_DrawFuzzColumnLow;
transcolfunc = R_DrawTranslatedColumnLow;
spanfunc = R_DrawSpanLow;
}
R_InitBuffer (scaledviewwidth, viewheight);
R_InitTextureMapping ();
// psprite scales
// Relative to ORIGWIDTH (the resolution weapon sprites were authored
// for), not SCREENWIDTH (the internal framebuffer size, which can
// now be larger - see i_video.h). These used to be the same number.
pspritescale = FRACUNIT*viewwidth/ORIGWIDTH;
pspriteiscale = FRACUNIT*ORIGWIDTH/viewwidth;
// thing clipping
for (i=0 ; i<viewwidth ; i++)
screenheightarray[i] = viewheight;
// planes
for (i=0 ; i<viewheight ; i++)
{
dy = ((i-viewheight/2)<<FRACBITS)+FRACUNIT/2;
dy = abs(dy);
yslope[i] = FixedDiv ( (viewwidth<<detailshift)/2*FRACUNIT, dy);
}
for (i=0 ; i<viewwidth ; i++)
{
cosadj = abs(finecosine[xtoviewangle[i]>>ANGLETOFINESHIFT]);
distscale[i] = FixedDiv (FRACUNIT,cosadj);
}
// Calculate the light levels to use
// for each level / scale combination.
for (i=0 ; i< LIGHTLEVELS ; i++)
{
startmap = ((LIGHTLEVELS-1-i)*2)*NUMCOLORMAPS/LIGHTLEVELS;
for (j=0 ; j<MAXLIGHTSCALE ; j++)
{
// Relative to ORIGWIDTH, not SCREENWIDTH - see pspritescale above.
level = startmap - j*ORIGWIDTH/(viewwidth<<detailshift)/DISTMAP;
if (level < 0)
level = 0;
if (level >= NUMCOLORMAPS)
level = NUMCOLORMAPS-1;
scalelight[i][j] = colormaps + level*256;
}
}
}
//
// R_ExecuteSetViewSize
//
void R_ExecuteSetViewSize (void)
{
setsizeneeded = false;
if (setblocks == 11)
{
scaledviewwidth = SCREENWIDTH;
viewheight = SCREENHEIGHT;
}
else
{
// setblocks*32 and *168/10 are native ORIGWIDTH x ORIGHEIGHT
// (320x200) formulas - vanilla's only resolution, back when that
// was also SCREENWIDTH/SCREENHEIGHT (see i_video.h for why it no
// longer is). Scale the result (after vanilla's own rounding, so
// the step granularity this produces is unchanged) or most of the
// 3-10 range collapses into tiny, barely-visible increments on a
// larger buffer before jumping straight to fullscreen at 11.
scaledviewwidth = (setblocks*32) * (SCREENWIDTH / ORIGWIDTH);
viewheight = ((setblocks*168/10)&~7) * (SCREENHEIGHT / ORIGHEIGHT);
}
detailshift = setdetail;
R_SetupViewSizeTables ();
}
//
// R_SetSplitscreenViewSize
// Like R_ExecuteSetViewSize, but for a splitscreen tile of an explicit
// pixel size instead of one derived from the setblocks status-bar-size
// setting. Always full detail (no low-detail mode). Only needs to be
// called when the tile size changes (i.e. the active player count
// changes), not every frame - R_RepositionBuffer handles repositioning
// same-sized tiles cheaply.
//
void R_SetSplitscreenViewSize (int width, int height)
{
scaledviewwidth = width;
viewheight = height;
detailshift = 0;
R_SetupViewSizeTables ();
}
//
// R_Init
//
void R_Init (void)
{
R_InitData ();
printf (".");
R_InitPointToAngle ();
printf (".");
R_InitTables ();
// viewwidth / viewheight / detailLevel are set by the defaults
printf (".");
R_SetViewSize (screenblocks, detailLevel);
R_InitPlanes ();
printf (".");
R_InitLightTables ();
printf (".");
R_InitSkyMap ();
R_InitTranslationTables ();
printf (".");
framecount = 0;
}
//
// R_PointInSubsector
//
subsector_t*
R_PointInSubsector
( fixed_t x,
fixed_t y )
{
node_t* node;
int side;
int nodenum;
// single subsector is a special case
if (!numnodes)
return subsectors;
nodenum = numnodes-1;
while (! (nodenum & NF_SUBSECTOR) )
{
node = &nodes[nodenum];
side = R_PointOnSide (x, y, node);
nodenum = node->children[side];
}
return &subsectors[nodenum & ~NF_SUBSECTOR];
}
//
// R_SetupFrame
//
void R_SetupFrame (player_t* player)
{
int i;
viewplayer = player;
viewx = player->mo->x;
viewy = player->mo->y;
viewangle = player->mo->angle + viewangleoffset;
extralight = player->extralight;
viewz = player->viewz;
viewsin = finesine[viewangle>>ANGLETOFINESHIFT];
viewcos = finecosine[viewangle>>ANGLETOFINESHIFT];
sscount = 0;
if (player->fixedcolormap)
{
fixedcolormap =
colormaps
+ player->fixedcolormap*256;
walllights = scalelightfixed;
for (i=0 ; i<MAXLIGHTSCALE ; i++)
scalelightfixed[i] = fixedcolormap;
}
else
fixedcolormap = 0;
framecount++;
validcount++;
}
//
// R_RenderView
//
void R_RenderPlayerView (player_t* player)
{
R_SetupFrame (player);
// Clear buffers.
R_ClearClipSegs ();
R_ClearDrawSegs ();
R_ClearPlanes ();
R_ClearSprites ();
// check for new console commands.
NetUpdate ();
// The head node is the last node output.
R_RenderBSPNode (numnodes-1);
// Check for new console commands.
NetUpdate ();
R_DrawPlanes ();
// Check for new console commands.
NetUpdate ();
R_DrawMasked ();
// Check for new console commands.
NetUpdate ();
}
//
// R_ComputeSplitLayout
// Computes a near-square grid (cols x rows, cols*rows >= numplayers) used
// to tile numplayers viewports into the screen for splitscreen.
//
void R_ComputeSplitLayout (int numplayers, int *cols, int *rows)
{
if (numplayers < 1)
numplayers = 1;
*cols = (int) ceil (sqrt ((double) numplayers));
*rows = (numplayers + *cols - 1) / *cols;
}
// The vanilla fullscreen aspect ratio (setblocks==11 in
// R_ExecuteSetViewSize). Several parts of the renderer that vanilla only
// ever exercised at this ratio - most visibly R_DrawPSprite's weapon
// sprite placement, which measures from a hardcoded SCREENWIDTH/2 center
// and BASEYCENTER (SCREENHEIGHT/2) rather than the current tile's actual
// width/height - assume it, rather than deriving correctly from
// whatever the current view's width:height happens to be. A splitscreen
// grid cell is generally NOT this ratio (e.g. a 2-player 2x1 layout
// gives cells twice as wide as they are tall relative to a full view),
// and stretching the rendered view to fill such a cell throws off that
// hardcoded weapon-sprite math. So instead we fit an aspect-correct view
// inside each cell and letterbox/pillarbox the remainder, rather than
// widening the handful of vanilla formulas that assume this ratio.
#define SPLITVIEW_ASPECT_NUM 320
#define SPLITVIEW_ASPECT_DEN 200
static void
R_FitAspectRect
( int cellx,
int celly,
int cellw,
int cellh,
int* vx,
int* vy,
int* vw,
int* vh )
{
int fitw;
fitw = cellh * SPLITVIEW_ASPECT_NUM / SPLITVIEW_ASPECT_DEN;
if (fitw <= cellw)
{
*vh = cellh;
*vw = fitw;
}
else
{
*vw = cellw;
*vh = cellw * SPLITVIEW_ASPECT_DEN / SPLITVIEW_ASPECT_NUM;
}
*vx = cellx + (cellw - *vw) / 2;
*vy = celly + (cellh - *vh) / 2;
}
//
// R_GetSplitscreenTile
// If splitscreen is active (more than one player in-game) and playernum
// is in-game, computes the screen-buffer rectangle that player's
// aspect-correct viewport occupies (see R_FitAspectRect) and returns
// true. Otherwise returns false, meaning the caller should fall back to
// the normal single fullscreen view. Shared by R_RenderSplitViews and
// the per-tile HUD in d_main.c so both agree on the same layout.
//
boolean
R_GetSplitscreenTile
( int playernum,
int* x,
int* y,
int* w,
int* h )
{
int active[MAXPLAYERS];
int numactive;
int index;
int cols, rows;
int cellw, cellh;
int i;
boolean qr_slot;
int layout_count;
if (!playeringame[playernum])
return false;
numactive = 0;
index = -1;
for (i=0 ; i<MAXPLAYERS ; i++)
{
if (playeringame[i])
{
if (i == playernum)
index = numactive;
active[numactive++] = i;
}
}
// Keep in step with R_RenderSplitViews's own reserved-QR-cell
// layout (see its comment) - otherwise this would report a plain
// fullscreen tile for the 1-active-player-plus-QR case that
// function actually renders as a 2-cell grid.
qr_slot = I_WebInputQrEnabled () && numactive < MAXPLAYERS;
if (numactive <= 1 && !qr_slot)
return false;
layout_count = numactive + (qr_slot ? 1 : 0);
R_ComputeSplitLayout (layout_count, &cols, &rows);
cellw = SCREENWIDTH / cols;
cellh = SCREENHEIGHT / rows;
R_FitAspectRect ((index % cols) * cellw, (index / cols) * cellh,
cellw, cellh, x, y, w, h);
return true;
}
// Thickness in pixels of the per-player colored border drawn around each
// splitscreen tile (see R_PlayerBorderColor).
#define SPLITVIEW_BORDER_THICKNESS 3
static void R_DrawSplitBorder (int x, int y, int w, int h, byte color)
{
int t;
if (w <= 2*SPLITVIEW_BORDER_THICKNESS || h <= 2*SPLITVIEW_BORDER_THICKNESS)
return;
for (t = 0; t < SPLITVIEW_BORDER_THICKNESS; t++)
V_DrawBox (x+t, y+t, w-2*t, h-2*t, color);
}
//
// R_RenderSplitViews
// Renders one viewport per in-game player into its own tile of the
// screen buffer. Called instead of R_RenderPlayerView when splitscreen
// is active (see d_main.c).
//
// Tracks the player count R_RenderSplitViews last sized its tiles for,
// so it only re-runs the (relatively expensive) R_SetSplitscreenViewSize
// when that count actually changes. File-level rather than a function-
// local static so R_InvalidateSplitViewCache (see below) can reset it
// from outside: D_Display stops calling R_RenderSplitViews at all once
// splitscreen goes false (it calls R_RenderPlayerView instead), so this
// function never gets a chance to notice "we dropped to 1 player" and
// reset the cache itself - see R_InvalidateSplitViewCache's callers.
static int split_last_numactive = -1;
void R_InvalidateSplitViewCache (void)
{
split_last_numactive = -1;
}
void R_RenderSplitViews (void)
{
int numactive;
int i;
int index;
int cols, rows;
int cellw, cellh;
int x, y, w, h;
boolean qr_slot;
int layout_count;
numactive = 0;
for (i=0 ; i<MAXPLAYERS ; i++)
if (playeringame[i])
numactive++;
if (numactive == 0)
return;
// Reserve one extra grid cell for a join QR whenever -qrstart wants
// one and the grid isn't already full (see I_WebInputQrEnabled) -
// including at exactly 1 active player, so the very first player to
// join still sees an invite to bring in the next one instead of a
// plain fullscreen view with no way to tell anyone how to join. As
// more players join, this shrinks by one slot each time exactly
// like any other cell the qr-fill check below reclaims, until the
// grid is completely full and there's no room left to reserve.
qr_slot = I_WebInputQrEnabled () && numactive < MAXPLAYERS;
if (numactive == 1 && !qr_slot)
{
// Only one active player and no QR to make room for - render
// fullscreen as usual.
R_RenderPlayerView (&players[displayplayer]);
return;
}
layout_count = numactive + (qr_slot ? 1 : 0);
R_ComputeSplitLayout (layout_count, &cols, &rows);
cellw = SCREENWIDTH / cols;
cellh = SCREENHEIGHT / rows;
if (numactive != split_last_numactive)
{
R_FitAspectRect (0, 0, cellw, cellh, &x, &y, &w, &h);
R_SetSplitscreenViewSize (w, h);
split_last_numactive = numactive;
}
// Blacken the whole screen first, not just each active player's own
// cell: cols*rows can exceed numactive (e.g. 5 players is a 3x2
// grid with one empty cell), and integer division can leave a
// sliver uncovered by any cell (e.g. 640/3 rounds down). Neither is
// ever touched by the per-player loop below, so without this they
// keep showing whatever an earlier, differently-shaped layout last
// rendered there (splitscreen skips R_FillBackScreen, which would
// normally paint over stale pixels between frames).
V_DrawFilledBox (0, 0, SCREENWIDTH, SCREENHEIGHT, 0);
index = 0;
for (i=0 ; i<MAXPLAYERS ; i++)
{
int cellx, celly;
if (!playeringame[i])
continue;
cellx = (index % cols) * cellw;
celly = (index / cols) * cellh;
R_FitAspectRect (cellx, celly, cellw, cellh, &x, &y, &w, &h);
R_RepositionBuffer (x, y, w, h);
R_RenderPlayerView (&players[i]);
// Border hugs the actual (aspect-correct) viewport, not the
// whole cell - letterbox/pillarbox bars stay plain black.
R_DrawSplitBorder (x, y, w, h, R_PlayerBorderColor (i));
// The classic status bar setting (Options > Screen Size) has
// nothing to drive while show_split is active - R_ExecuteSetViewSize
// (which reads it) is skipped in favor of R_SetSplitscreenViewSize
// (see d_main.c). Wire it back up as the on/off switch for this
// mini HUD instead, same as vanilla: max size (blocks==11) hides
// it for an unobstructed view, anything below shows it.
if (screenblocks != 11)
ST_DrawMiniHud (&players[i], x, y, w, h);
index++;
}
// qr_slot being true guarantees cols*rows > index (the grid was
// deliberately sized with room for it - see layout_count above),
// so there's always a cell here left over from the per-player loop
// to put it in.
if (qr_slot)
{
int cellx = (index % cols) * cellw;
int celly = (index / cols) * cellh;
I_WebInputDrawJoinQR (cellx, celly, cellw, cellh);
}
}