// Emacs style mode select -*- C++ -*- //----------------------------------------------------------------------------- // // $Id:$ // // Copyright (C) 1993-1996 by id Software, Inc. // // This source is available for distribution and/or modification // only under the terms of the DOOM Source Code License as // published by id Software. All rights reserved. // // The source is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // FITNESS FOR A PARTICULAR PURPOSE. See the DOOM Source Code License // for more details. // // $Log:$ // // DESCRIPTION: // Rendering main loop and setup functions, // utility functions (BSP, geometry, trigonometry). // See tables.c, too. // //----------------------------------------------------------------------------- // HEADER FILES ------------------------------------------------------------ #include #include #include "templates.h" #include "doomdef.h" #include "d_net.h" #include "doomstat.h" #include "m_random.h" #include "m_bbox.h" #include "r_local.h" #include "r_plane.h" #include "r_bsp.h" #include "r_segs.h" #include "r_3dfloors.h" #include "r_sky.h" #include "st_stuff.h" #include "c_cvars.h" #include "c_dispatch.h" #include "v_video.h" #include "stats.h" #include "i_video.h" #include "i_system.h" #include "a_sharedglobal.h" #include "r_data/r_translate.h" #include "p_3dmidtex.h" #include "r_data/r_interpolate.h" #include "v_palette.h" #include "po_man.h" #include "p_effect.h" #include "st_start.h" #include "v_font.h" #include "r_data/colormaps.h" #include "farchive.h" // MACROS ------------------------------------------------------------------ #if 0 #define TEST_X 32343794 #define TEST_Y 111387517 #define TEST_Z 2164524 #define TEST_ANGLE 2468347904 #endif // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- void R_SpanInitData (); void R_DeinitSprites(); // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- static void R_ShutdownRenderer(); // EXTERNAL DATA DECLARATIONS ---------------------------------------------- extern short *openings; extern bool r_fakingunderwater; extern "C" int fuzzviewheight; extern subsector_t *InSubsector; extern bool r_showviewer; // PRIVATE DATA DECLARATIONS ----------------------------------------------- static float CurrentVisibility = 8.f; static fixed_t MaxVisForWall; static fixed_t MaxVisForFloor; bool r_dontmaplines; // PUBLIC DATA DEFINITIONS ------------------------------------------------- CVAR (String, r_viewsize, "", CVAR_NOSET) CVAR (Bool, r_shadercolormaps, true, CVAR_ARCHIVE) fixed_t r_BaseVisibility; fixed_t r_WallVisibility; fixed_t r_FloorVisibility; float r_TiltVisibility; fixed_t r_SpriteVisibility; fixed_t r_ParticleVisibility; fixed_t r_SkyVisibility; fixed_t GlobVis; fixed_t viewingrangerecip; fixed_t FocalLengthX; fixed_t FocalLengthY; float FocalLengthXfloat; FDynamicColormap*basecolormap; // [RH] colormap currently drawing with int fixedlightlev; lighttable_t *fixedcolormap; FSpecialColormap *realfixedcolormap; float WallTMapScale2; bool bRenderingToCanvas; // [RH] True if rendering to a special canvas fixed_t globaluclip, globaldclip; fixed_t centerxfrac; fixed_t centeryfrac; fixed_t yaspectmul; fixed_t baseyaspectmul; // yaspectmul without a forced aspect ratio float iyaspectmulfloat; fixed_t InvZtoScale; // just for profiling purposes int linecount; int loopcount; // // precalculated math tables // // The xtoviewangleangle[] table maps a screen pixel // to the lowest viewangle that maps back to x ranges // from clipangle to -clipangle. angle_t xtoviewangle[MAXWIDTH+1]; bool foggy; // [RH] ignore extralight and fullbright? int r_actualextralight; void (*colfunc) (void); void (*basecolfunc) (void); void (*fuzzcolfunc) (void); void (*transcolfunc) (void); void (*spanfunc) (void); void (*hcolfunc_pre) (void); void (*hcolfunc_post1) (int hx, int sx, int yl, int yh); void (*hcolfunc_post2) (int hx, int sx, int yl, int yh); void (*hcolfunc_post4) (int sx, int yl, int yh); cycle_t WallCycles, PlaneCycles, MaskedCycles, WallScanCycles; // PRIVATE DATA DEFINITIONS ------------------------------------------------ static int lastcenteryfrac; // CODE -------------------------------------------------------------------- //========================================================================== // // viewangletox // // Used solely for construction the xtoviewangle table. // //========================================================================== static inline int viewangletox(int i) { if (finetangent[i] > FRACUNIT*2) { return -1; } else if (finetangent[i] < -FRACUNIT*2) { return viewwidth+1; } else { int t = FixedMul(finetangent[i], FocalLengthX); t = (centerxfrac - t + FRACUNIT-1) >> FRACBITS; return clamp(t, -1, viewwidth+1); } } //========================================================================== // // R_InitTextureMapping // //========================================================================== void R_InitTextureMapping () { int i, x; // Calc focallength so FieldOfView fineangles covers viewwidth. FocalLengthX = FixedDiv (centerxfrac, FocalTangent); FocalLengthY = Scale (centerxfrac, yaspectmul, FocalTangent); FocalLengthXfloat = (float)FocalLengthX / 65536.f; // This is 1/FocalTangent before the widescreen extension of FOV. viewingrangerecip = DivScale32(1, finetangent[FINEANGLES/4+(FieldOfView/2)]); // [RH] Do not generate viewangletox, because texture mapping is no // longer done with trig, so it's not needed. // Now generate xtoviewangle for sky texture mapping. // We do this with a hybrid approach: The center 90 degree span is // constructed as per the original code: // Scan xtoviewangle to find the smallest view angle that maps to x. // (viewangletox is sorted in non-increasing order.) // This reduces the chances of "doubling-up" of texture columns in // the drawn sky texture. // The remaining arcs are done with tantoangle instead. const int t1 = MAX(centerx - (FocalLengthX >> FRACBITS), 0); const int t2 = MIN(centerx + (FocalLengthX >> FRACBITS), viewwidth); const fixed_t dfocus = FocalLengthX >> DBITS; for (i = 0, x = t2; x >= t1; --x) { while(viewangletox(i) > x) { ++i; } xtoviewangle[x] = (i << ANGLETOFINESHIFT) - ANGLE_90; } for (x = t2 + 1; x <= viewwidth; ++x) { xtoviewangle[x] = ANGLE_270 + tantoangle[dfocus / (x - centerx)]; } for (x = 0; x < t1; ++x) { xtoviewangle[x] = (angle_t)(-(signed)xtoviewangle[viewwidth - x]); } } //========================================================================== // // R_SetVisibility // // Changes how rapidly things get dark with distance // //========================================================================== void R_SetVisibility (float vis) { // Allow negative visibilities, just for novelty's sake vis = clamp (vis, -204.7f, 204.7f); // (205 and larger do not work in 5:4 aspect ratio) CurrentVisibility = vis; if (FocalTangent == 0 || FocalLengthY == 0) { // If r_visibility is called before the renderer is all set up, don't // divide by zero. This will be called again later, and the proper // values can be initialized then. return; } r_BaseVisibility = xs_RoundToInt(vis * 65536.f); // Prevent overflow on walls if (r_BaseVisibility < 0 && r_BaseVisibility < -MaxVisForWall) r_WallVisibility = -MaxVisForWall; else if (r_BaseVisibility > 0 && r_BaseVisibility > MaxVisForWall) r_WallVisibility = MaxVisForWall; else r_WallVisibility = r_BaseVisibility; r_WallVisibility = FixedMul (Scale (InvZtoScale, SCREENWIDTH*BaseRatioSizes[WidescreenRatio][1], viewwidth*SCREENHEIGHT*3), FixedMul (r_WallVisibility, FocalTangent)); // Prevent overflow on floors/ceilings. Note that the calculation of // MaxVisForFloor means that planes less than two units from the player's // view could still overflow, but there is no way to totally eliminate // that while still using fixed point math. if (r_BaseVisibility < 0 && r_BaseVisibility < -MaxVisForFloor) r_FloorVisibility = -MaxVisForFloor; else if (r_BaseVisibility > 0 && r_BaseVisibility > MaxVisForFloor) r_FloorVisibility = MaxVisForFloor; else r_FloorVisibility = r_BaseVisibility; r_FloorVisibility = Scale (160*FRACUNIT, r_FloorVisibility, FocalLengthY); r_TiltVisibility = vis * (float)FocalTangent * (16.f * 320.f) / (float)viewwidth; r_SpriteVisibility = r_WallVisibility; } //========================================================================== // // R_GetVisibility // //========================================================================== float R_GetVisibility () { return CurrentVisibility; } //========================================================================== // // CCMD r_visibility // // Controls how quickly light ramps across a 1/z range. Set this, and it // sets all the r_*Visibility variables (except r_SkyVisibilily, which is // currently unused). // //========================================================================== CCMD (r_visibility) { if (argv.argc() < 2) { Printf ("Visibility is %g\n", R_GetVisibility()); } else if (!netgame) { R_SetVisibility ((float)atof (argv[1])); } else { Printf ("Visibility cannot be changed in net games.\n"); } } //========================================================================== // // R_SetWindow // //========================================================================== void R_SWRSetWindow(int windowSize, int fullWidth, int fullHeight, int stHeight, int trueratio) { int virtheight, virtwidth, virtwidth2, virtheight2; if (!bRenderingToCanvas) { // Set r_viewsize cvar to reflect the current view size UCVarValue value; char temp[16]; mysnprintf (temp, countof(temp), "%d x %d", viewwidth, viewheight); value.String = temp; r_viewsize.ForceSet (value, CVAR_String); } fuzzviewheight = viewheight - 2; // Maximum row the fuzzer can draw to halfviewwidth = (viewwidth >> 1) - 1; lastcenteryfrac = 1<<30; centerxfrac = centerx<mo) { if (player->fixedcolormap >= 0 && player->fixedcolormap < (int)SpecialColormaps.Size()) { realfixedcolormap = &SpecialColormaps[player->fixedcolormap]; if (RenderTarget == screen && (DFrameBuffer *)screen->Accel2D && r_shadercolormaps) { // Render everything fullbright. The copy to video memory will // apply the special colormap, so it won't be restricted to the // palette. fixedcolormap = realcolormaps; } else { fixedcolormap = SpecialColormaps[player->fixedcolormap].Colormap; } } else if (player->fixedlightlevel >= 0 && player->fixedlightlevel < NUMCOLORMAPS) { fixedlightlev = player->fixedlightlevel * 256; } } // [RH] Inverse light for shooting the Sigil if (fixedcolormap == NULL && extralight == INT_MIN) { fixedcolormap = SpecialColormaps[INVERSECOLORMAP].Colormap; extralight = 0; } } //========================================================================== // // R_SetupFreelook // // [RH] freelook stuff // //========================================================================== void R_SetupFreelook() { { fixed_t dy; if (camera != NULL) { dy = FixedMul (FocalLengthY, finetangent[(ANGLE_90-viewpitch)>>ANGLETOFINESHIFT]); } else { dy = 0; } centeryfrac = (viewheight << (FRACBITS-1)) + dy; centery = centeryfrac >> FRACBITS; globaluclip = FixedDiv (-centeryfrac, InvZtoScale); globaldclip = FixedDiv ((viewheight<GetBuffer(); // top edge for (int x = pds->x1; x < pds->x2; x++) { if (x < 0 || x >= RenderTarget->GetWidth()) continue; int p = x - pds->x1; int Ytop = pds->ceilingclip[p]; int Ybottom = pds->floorclip[p]; if (x == pds->x1 || x == pds->x2-1) { RenderTarget->DrawLine(x, Ytop, x, Ybottom+1, color, 0); continue; } int YtopPrev = pds->ceilingclip[p-1]; int YbottomPrev = pds->floorclip[p-1]; if (abs(Ytop-YtopPrev) > 1) RenderTarget->DrawLine(x, YtopPrev, x, Ytop, color, 0); else *(pixels + Ytop * RenderTarget->GetPitch() + x) = color; if (abs(Ybottom-YbottomPrev) > 1) RenderTarget->DrawLine(x, YbottomPrev, x, Ybottom, color, 0); else *(pixels + Ybottom * RenderTarget->GetPitch() + x) = color; } } void R_EnterPortal (PortalDrawseg* pds, int depth) { // [ZZ] check depth. fill portal with black if it's exceeding the visual recursion limit, and continue like nothing happened. if (depth >= r_portal_recursions) { BYTE color = (BYTE)BestColor((DWORD *)GPalette.BaseColors, 0, 0, 0, 0, 255); int spacing = RenderTarget->GetPitch(); for (int x = pds->x1; x < pds->x2; x++) { if (x < 0 || x >= RenderTarget->GetWidth()) continue; int Ytop = pds->ceilingclip[x-pds->x1]; int Ybottom = pds->floorclip[x-pds->x1]; BYTE *dest = RenderTarget->GetBuffer() + x + Ytop * spacing; for (int y = Ytop; y <= Ybottom; y++) { *dest = color; dest += spacing; } } if (r_highlight_portals) R_HighlightPortal(pds); return; } angle_t startang = viewangle; fixed_t startx = viewx; fixed_t starty = viewy; fixed_t startz = viewz; DVector3 savedpath[2] = { ViewPath[0], ViewPath[1] }; ActorRenderFlags savedvisibility = camera? camera->renderflags & RF_INVISIBLE : ActorRenderFlags::FromInt(0); CurrentPortalUniq++; unsigned int portalsAtStart = WallPortals.Size (); if (pds->mirror) { //vertex_t *v1 = ds->curline->v1; vertex_t *v1 = pds->src->v1; // Reflect the current view behind the mirror. if (pds->src->Delta().X == 0) { // vertical mirror viewx = v1->fixX() - startx + v1->fixX(); } else if (pds->src->Delta().Y == 0) { // horizontal mirror viewy = v1->fixY() - starty + v1->fixY(); } else { // any mirror--use floats to avoid integer overflow vertex_t *v2 = pds->src->v2; double dx = v2->fX() - v1->fX(); double dy = v2->fY() - v1->fY(); double x1 = v1->fX(); double y1 = v1->fY(); double x = FIXED2DBL(startx); double y = FIXED2DBL(starty); // the above two cases catch len == 0 double r = ((x - x1)*dx + (y - y1)*dy) / (dx*dx + dy*dy); viewx = FLOAT2FIXED((x1 + r * dx)*2 - x); viewy = FLOAT2FIXED((y1 + r * dy)*2 - y); } viewangle = pds->src->Delta().Angle().BAMs() - startang; } else { DVector3 view(FIXED2DBL(viewx), FIXED2DBL(viewy), FIXED2DBL(viewz)); DAngle va = ANGLE2DBL(viewangle); P_TranslatePortalXY(pds->src, view.X, view.Y); P_TranslatePortalZ(pds->src, view.Z); P_TranslatePortalAngle(pds->src, va); P_TranslatePortalXY(pds->src, ViewPath[0].X, ViewPath[0].Y); P_TranslatePortalXY(pds->src, ViewPath[1].X, ViewPath[1].Y); viewx = FLOAT2FIXED(view.X); viewy = FLOAT2FIXED(view.Y); viewz = FLOAT2FIXED(view.Z); viewangle = va.BAMs(); if (!r_showviewer && camera) { double distp = (ViewPath[0] - ViewPath[1]).Length(); if (distp > EQUAL_EPSILON) { double dist1 = (view - ViewPath[0]).Length(); double dist2 = (view - ViewPath[1]).Length(); if (dist1 + dist2 < distp + 1) { camera->renderflags |= RF_INVISIBLE; } } } } ViewAngle = AngleToFloat(viewangle); ViewPos = { FIXED2DBL(viewx), FIXED2DBL(viewy), FIXED2DBL(viewz) }; viewsin = finesine[viewangle>>ANGLETOFINESHIFT]; viewcos = finecosine[viewangle>>ANGLETOFINESHIFT]; viewtansin = FixedMul (FocalTangent, viewsin); viewtancos = FixedMul (FocalTangent, viewcos); R_CopyStackedViewParameters(); validcount++; PortalDrawseg* prevpds = CurrentPortal; CurrentPortal = pds; R_ClearPlanes (false); R_ClearClipSegs (pds->x1, pds->x2); WindowLeft = pds->x1; WindowRight = pds->x2; // RF_XFLIP should be removed before calling the root function int prevmf = MirrorFlags; if (pds->mirror) { if (MirrorFlags & RF_XFLIP) MirrorFlags &= ~RF_XFLIP; else MirrorFlags |= RF_XFLIP; } // some portals have height differences, account for this here R_3D_EnterSkybox(); // push 3D floor height map CurrentPortalInSkybox = false; // first portal in a skybox should set this variable to false for proper clipping in skyboxes. // first pass, set clipping memcpy (ceilingclip + pds->x1, &pds->ceilingclip[0], pds->len*sizeof(*ceilingclip)); memcpy (floorclip + pds->x1, &pds->floorclip[0], pds->len*sizeof(*floorclip)); InSubsector = NULL; R_RenderBSPNode (nodes + numnodes - 1); R_3D_ResetClip(); // reset clips (floor/ceiling) if (!savedvisibility && camera) camera->renderflags &= ~RF_INVISIBLE; PlaneCycles.Clock(); R_DrawPlanes (); R_DrawPortals (); PlaneCycles.Unclock(); fixed_t vzp = viewz; int prevuniq = CurrentPortalUniq; // depth check is in another place right now unsigned int portalsAtEnd = WallPortals.Size (); for (; portalsAtStart < portalsAtEnd; portalsAtStart++) { R_EnterPortal (&WallPortals[portalsAtStart], depth + 1); } int prevuniq2 = CurrentPortalUniq; CurrentPortalUniq = prevuniq; NetUpdate(); MaskedCycles.Clock(); // [ZZ] count sprites in portals/mirrors along with normal ones. R_DrawMasked (); // this is required since with portals there often will be cases when more than 80% of the view is inside a portal. MaskedCycles.Unclock(); NetUpdate(); R_3D_LeaveSkybox(); // pop 3D floor height map CurrentPortalUniq = prevuniq2; // draw a red line around a portal if it's being highlighted if (r_highlight_portals) R_HighlightPortal(pds); CurrentPortal = prevpds; MirrorFlags = prevmf; viewangle = startang; viewx = startx; viewy = starty; viewz = startz; ViewPath[0] = savedpath[0]; ViewPath[1] = savedpath[1]; ViewAngle = AngleToFloat(viewangle); ViewPos = { FIXED2DBL(viewx), FIXED2DBL(viewy), FIXED2DBL(viewz) }; } //========================================================================== // // R_SetupBuffer // // Precalculate all row offsets and fuzz table. // //========================================================================== void R_SetupBuffer () { static BYTE *lastbuff = NULL; int pitch = RenderTarget->GetPitch(); BYTE *lineptr = RenderTarget->GetBuffer() + viewwindowy*pitch + viewwindowx; if (dc_pitch != pitch || lineptr != lastbuff) { if (dc_pitch != pitch) { dc_pitch = pitch; R_InitFuzzTable (pitch); #if defined(X86_ASM) || defined(X64_ASM) ASM_PatchPitch (); #endif } dc_destorg = lineptr; for (int i = 0; i < RenderTarget->GetHeight(); i++) { ylookup[i] = i * pitch; } } } //========================================================================== // // R_RenderActorView // //========================================================================== void R_RenderActorView (AActor *actor, bool dontmaplines) { WallCycles.Reset(); PlaneCycles.Reset(); MaskedCycles.Reset(); WallScanCycles.Reset(); fakeActive = 0; // kg3D - reset fake floor indicator R_3D_ResetClip(); // reset clips (floor/ceiling) R_SetupBuffer (); R_SetupFrame (actor); // Clear buffers. R_ClearClipSegs (0, viewwidth); R_ClearDrawSegs (); R_ClearPlanes (true); R_ClearSprites (); NetUpdate (); // [RH] Show off segs if r_drawflat is 1 if (r_drawflat) { hcolfunc_pre = R_FillColumnHorizP; hcolfunc_post1 = rt_copy1col; hcolfunc_post4 = rt_copy4cols; colfunc = R_FillColumnP; spanfunc = R_FillSpan; } else { hcolfunc_pre = R_DrawColumnHoriz; hcolfunc_post1 = rt_map1col; hcolfunc_post4 = rt_map4cols; colfunc = basecolfunc; spanfunc = R_DrawSpan; } WindowLeft = 0; WindowRight = viewwidth; MirrorFlags = 0; CurrentPortal = NULL; CurrentPortalUniq = 0; r_dontmaplines = dontmaplines; // [RH] Hack to make windows into underwater areas possible r_fakingunderwater = false; // [RH] Setup particles for this frame P_FindParticleSubsectors (); WallCycles.Clock(); ActorRenderFlags savedflags = camera->renderflags; // Never draw the player unless in chasecam mode if (!r_showviewer) { camera->renderflags |= RF_INVISIBLE; } // Link the polyobjects right before drawing the scene to reduce the amounts of calls to this function PO_LinkToSubsectors(); InSubsector = NULL; R_RenderBSPNode (nodes + numnodes - 1); // The head node is the last node output. R_3D_ResetClip(); // reset clips (floor/ceiling) camera->renderflags = savedflags; WallCycles.Unclock(); NetUpdate (); if (viewactive) { PlaneCycles.Clock(); R_DrawPlanes (); R_DrawPortals (); PlaneCycles.Unclock(); // [RH] Walk through mirrors // [ZZ] Merged with portals size_t lastportal = WallPortals.Size(); for (unsigned int i = 0; i < lastportal; i++) { R_EnterPortal(&WallPortals[i], 0); } CurrentPortal = NULL; CurrentPortalUniq = 0; NetUpdate (); MaskedCycles.Clock(); R_DrawMasked (); MaskedCycles.Unclock(); NetUpdate (); } WallPortals.Clear (); interpolator.RestoreInterpolations (); R_SetupBuffer (); // If we don't want shadered colormaps, NULL it now so that the // copy to the screen does not use a special colormap shader. if (!r_shadercolormaps) { realfixedcolormap = NULL; } } //========================================================================== // // R_RenderViewToCanvas // // Pre: Canvas is already locked. // //========================================================================== void R_RenderViewToCanvas (AActor *actor, DCanvas *canvas, int x, int y, int width, int height, bool dontmaplines) { const bool savedviewactive = viewactive; viewwidth = width; RenderTarget = canvas; bRenderingToCanvas = true; R_SetWindow (12, width, height, height); viewwindowx = x; viewwindowy = y; viewactive = true; R_RenderActorView (actor, dontmaplines); RenderTarget = screen; bRenderingToCanvas = false; R_ExecuteSetViewSize (); screen->Lock (true); R_SetupBuffer (); screen->Unlock (); viewactive = savedviewactive; } //========================================================================== // // R_MultiresInit // // Called from V_SetResolution() // //========================================================================== void R_MultiresInit () { R_PlaneInitData (); } //========================================================================== // // STAT fps // // Displays statistics about rendering times // //========================================================================== extern cycle_t WallCycles, PlaneCycles, MaskedCycles, WallScanCycles; extern cycle_t FrameCycles; ADD_STAT (fps) { FString out; out.Format("frame=%04.1f ms walls=%04.1f ms planes=%04.1f ms masked=%04.1f ms", FrameCycles.TimeMS(), WallCycles.TimeMS(), PlaneCycles.TimeMS(), MaskedCycles.TimeMS()); return out; } static double f_acc, w_acc,p_acc,m_acc; static int acc_c; ADD_STAT (fps_accumulated) { f_acc += FrameCycles.TimeMS(); w_acc += WallCycles.TimeMS(); p_acc += PlaneCycles.TimeMS(); m_acc += MaskedCycles.TimeMS(); acc_c++; FString out; out.Format("frame=%04.1f ms walls=%04.1f ms planes=%04.1f ms masked=%04.1f ms %d counts", f_acc/acc_c, w_acc/acc_c, p_acc/acc_c, m_acc/acc_c, acc_c); Printf(PRINT_LOG, "%s\n", out.GetChars()); return out; } //========================================================================== // // STAT wallcycles // // Displays the minimum number of cycles spent drawing walls // //========================================================================== static double bestwallcycles = HUGE_VAL; ADD_STAT (wallcycles) { FString out; double cycles = WallCycles.Time(); if (cycles && cycles < bestwallcycles) bestwallcycles = cycles; out.Format ("%g", bestwallcycles); return out; } //========================================================================== // // CCMD clearwallcycles // // Resets the count of minimum wall drawing cycles // //========================================================================== CCMD (clearwallcycles) { bestwallcycles = HUGE_VAL; } #if 1 // To use these, also uncomment the clock/unclock in wallscan static double bestscancycles = HUGE_VAL; ADD_STAT (scancycles) { FString out; double scancycles = WallScanCycles.Time(); if (scancycles && scancycles < bestscancycles) bestscancycles = scancycles; out.Format ("%g", bestscancycles); return out; } CCMD (clearscancycles) { bestscancycles = HUGE_VAL; } #endif