// r_splitscreen.cpp -- see r_splitscreen.h.
#include "r_splitscreen.h"
#include <cmath>
#include "doomstat.h"
#include "d_player.h"
#include "d_main.h"
#include "i_time.h"
#include "v_video.h"
#include "v_draw.h"
#include "v_font.h"
#include "textures.h"
#include "gametexture.h"
#include "r_utility.h"
#include "scene/hw_drawinfo.h"
#include "hw_renderstate.h"
#include "flatvertices.h"
EXTERN_CVAR(Bool, ui_classic)
namespace
{
struct WJSplitCell
{
FGameTexture *gameTex = nullptr; // owns the wrapped FCanvasTexture (ref-counted)
FCanvasTexture *canvasTex = nullptr;
int width = 0, height = 0;
};
WJSplitCell wj_cells[MAXPLAYERS];
void FreeCell(WJSplitCell &cell)
{
if (cell.gameTex != nullptr)
{
delete cell.gameTex; // ref-counted Base releases the FCanvasTexture too
}
cell.gameTex = nullptr;
cell.canvasTex = nullptr;
cell.width = cell.height = 0;
}
void EnsureCell(int slot, int w, int h)
{
WJSplitCell &cell = wj_cells[slot];
if (cell.canvasTex != nullptr && cell.width == w && cell.height == h)
return;
FreeCell(cell);
cell.canvasTex = new FCanvasTexture(w, h);
cell.gameTex = MakeGameTexture(cell.canvasTex, nullptr, ETextureType::Wall);
cell.width = w;
cell.height = h;
}
// Lay out `count` cells to fill a widthxheight area as evenly as
// possible: pick the column count that keeps cells closest to the
// area's own aspect ratio, then divide rows evenly under that.
struct WJGridLayout
{
int cols, rows;
};
WJGridLayout ComputeGrid(int count, int width, int height)
{
if (count <= 1)
return { 1, 1 };
// Keep the grid itself close to square (cols ~= rows) rather than
// trying to match each cell's aspect to the window's. Matching the
// window aspect sounds right but isn't: for count=2 it picks a
// 1-column, 2-row stack, giving cells over 3x wider than tall on a
// normal widescreen window. The renderer's FOV/aspect math (mirrored
// from the normal single-view path, itself only ever exercised
// against sane window aspect ratios) isn't meant for cells that
// extreme, and geometry starts vanishing at the edges. A square-ish
// grid keeps every cell's aspect within shouting distance of the
// window's own, which is what that math actually assumes -- and
// incidentally matches how every real splitscreen game lays out 2
// players (side by side), not this engine being unusual.
int cols = (int)std::ceil(std::sqrt((double)count));
int rows = (count + cols - 1) / cols;
return { cols, rows };
}
// Player slots currently occupying a grid cell, in a stable order (by
// slot index) so a given player doesn't jump cells frame to frame.
int ActivePlayers(int *out)
{
int n = 0;
for (int i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && players[i].mo != nullptr)
out[n++] = i;
}
return n;
}
}
bool WJ_SplitscreenActive()
{
for (int i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && players[i].RemoteInputSlot)
return true;
}
return false;
}
void WJ_UpdateSplitscreenViews()
{
int active[MAXPLAYERS];
int count = ActivePlayers(active);
if (count == 0)
return;
// The normal single-view path (RenderView(player_t*) in hw_entrypoint.cpp)
// resets this shared per-frame vertex buffer once at the top, and every
// view rendered afterward that frame (camera textures, the main view)
// accumulates into it. Since we replace that whole function, we must
// do the same reset ourselves -- once per frame here, not once per
// player -- or the buffer fills up over a handful of frames and the
// renderer hits its "out of vertex memory" fatal error.
screen->RenderState()->SetVertexBuffer(screen->mVertexData);
screen->mVertexData->Reset();
int fullWidth = screen->GetWidth();
int fullHeight = screen->GetHeight();
WJGridLayout grid = ComputeGrid(count, fullWidth, fullHeight);
int cellW = fullWidth / grid.cols;
int cellH = fullHeight / grid.rows;
// hw_weapon.cpp's GetWeaponRect positions the weapon sprite using the
// GLOBAL viewwidth/viewheight/viewwindowx/y -- the normal single-view
// window size, which we never touch and which stays full-window-sized
// throughout splitscreen play. Left alone, every cell's weapon gets
// positioned as if it were rendering into the whole window, floating
// wherever that math lands within our much smaller cell instead of
// pinned to *this* cell's own bottom. The software renderer's
// RenderViewToCanvas (r_scene.cpp) has the same requirement and saves/
// overrides/restores these same globals around its own off-screen
// render for exactly this reason -- this mirrors that.
int savedViewwindowx = viewwindowx, savedViewwindowy = viewwindowy;
int savedViewwidth = viewwidth, savedViewheight = viewheight;
for (int n = 0; n < count; n++)
{
int slot = active[n];
EnsureCell(slot, cellW, cellH);
AActor *camera = players[slot].camera != nullptr ? players[slot].camera : players[slot].mo;
if (camera == nullptr)
continue;
float fov = (float)camera->GetFOV(I_GetTimeFrac());
float ratio = cellH > 0 ? float(cellW) / cellH : 1.f;
float fovratio = (ratio >= 1.3f) ? 1.333333f : ratio;
FCanvasTexture *canvasTex = wj_cells[slot].canvasTex;
screen->RenderTextureView(canvasTex, [&](IntRect &bounds)
{
// Canvas textures are always their own self-contained
// render target starting at (0,0) -- bounds.left/top are
// 0 here, not this cell's position within the final
// composited window (that offset is applied later, by
// WJ_DrawSplitscreenGrid's ordinary 2D blit).
viewwindowx = bounds.left;
viewwindowy = bounds.top;
viewwidth = bounds.width;
viewheight = bounds.height;
FRenderViewpoint texvp;
// toscreen=true (despite this being an offscreen canvas
// texture, not the window) is deliberate: it's what makes
// ProcessScene prepare this player's weapon sprite --
// see PreparePlayerSprites2D/3D in hw_weapon.cpp and the
// mainview=false handling in RenderViewpoint. Without it
// every cell renders the world but not the gun.
RenderViewpoint(texvp, camera, &bounds, fov, ratio, fovratio, false, true);
});
}
viewwindowx = savedViewwindowx;
viewwindowy = savedViewwindowy;
viewwidth = savedViewwidth;
viewheight = savedViewheight;
// The normal single-view path always finishes by rendering the main
// view with bounds=NULL (RenderViewpoint(..., nullptr, ..., true,
// true) in hw_entrypoint.cpp), which resets the GL viewport back to
// the full window -- that's *why* the per-camera-texture viewport
// changes above don't normally leak into the 2D/HUD pass that follows.
// We replace that whole call, so nothing else does this reset for us;
// without it the 2D pass inherits whichever player's cell-sized
// viewport was set last, and everything (including this grid) draws
// into that one small leftover rectangle instead of the full window.
screen->SetViewportRects(nullptr);
}
void WJ_DrawSplitscreenGrid()
{
int active[MAXPLAYERS];
int count = ActivePlayers(active);
if (count == 0)
return;
int fullWidth = twod->GetWidth();
int fullHeight = twod->GetHeight();
WJGridLayout grid = ComputeGrid(count, fullWidth, fullHeight);
int cellW = fullWidth / grid.cols;
int cellH = fullHeight / grid.rows;
for (int n = 0; n < count; n++)
{
int slot = active[n];
WJSplitCell &cell = wj_cells[slot];
if (cell.gameTex == nullptr)
continue;
int col = n % grid.cols;
int row = n / grid.cols;
int x = col * cellW;
int y = row * cellH;
// TODO verify empirically whether canvas-texture content needs
// DTA_FlipY here (FBO-rendered content is often Y-flipped relative
// to a normal 2D draw) -- check a screenshot before shipping this.
//
// DTA_VirtualWidth/Height must be passed alongside DTA_DestWidth/
// Height and set equal to the real canvas size: ParseDrawTextureTags
// resolves an unset virtual size from parms->viewport, but that
// field isn't populated until later in SetTextureParms, so leaving
// it unset reads a not-yet-initialized rect and produces a wildly
// wrong (tiny, mispositioned) destination rect.
DrawTexture(twod, cell.gameTex, x, y,
DTA_DestWidth, cellW, DTA_DestHeight, cellH,
DTA_VirtualWidth, fullWidth, DTA_VirtualHeight, fullHeight,
DTA_TopLeft, true,
TAG_DONE);
FFont *font = ui_classic ? SmallFont : NewSmallFont;
int health = players[slot].mo != nullptr ? players[slot].mo->health : 0;
FString label;
label.Format("%s %d", players[slot].userinfo.GetName(), health);
DrawText(twod, font, CR_WHITE, x + 4, y + cellH - font->GetHeight() - 4, label.GetChars(), TAG_DONE);
}
}