From 00d8da42f76d5e04a9d11e0deaac38391132808a Mon Sep 17 00:00:00 2001 From: divverent Date: Sun, 20 May 2007 14:59:03 +0000 Subject: [PATCH] new option -capturedemo to capture a demo to an AVI file; new cvars cl_capturevideo_width, cl_capturevideo_height to scale down video capture when capturing >1GB, don't write an index chunk (in that case, an ix00/ix01 chunk has to be written, but that's not yet implemented) when capturing, write a "dmlh" header to store the total number of frames, and store the partial number of frames in the "avih" header, as specified by OpenDML when capturing, store the aspect ratio in a "vprp" header chunk (OpenDML) make Host_Quit_f do nothing when already quitting (fixes "quit" issue while playing back a demo); note that Host_Quit_f is only used by the console and by -benchmark/-demo/-capturedemo git-svn-id: svn://svn.icculus.org/twilight/trunk/darkplaces@7321 d7cf8633-e32d-0410-b094-e92efae38249 --- cl_demo.c | 3 +- cl_main.c | 1 + cl_screen.c | 202 +++++++++++++++++++++++++++++++++++++++++++++------- cl_screen.h | 1 + client.h | 5 +- host.c | 16 ++++- host_cmd.c | 9 ++- 7 files changed, 208 insertions(+), 29 deletions(-) diff --git a/cl_demo.c b/cl_demo.c index 27761778..2485afb9 100644 --- a/cl_demo.c +++ b/cl_demo.c @@ -20,6 +20,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "quakedef.h" +extern cvar_t cl_capturevideo; int old_vsync = 0; void CL_FinishTimeDemo (void); @@ -87,7 +88,7 @@ void CL_StopPlayback (void) if (cls.timedemo) CL_FinishTimeDemo (); - if (COM_CheckParm("-demo")) + if (COM_CheckParm("-demo") || COM_CheckParm("-capturedemo")) Host_Quit_f(); } diff --git a/cl_main.c b/cl_main.c index d07b186a..8cba25a7 100644 --- a/cl_main.c +++ b/cl_main.c @@ -2146,6 +2146,7 @@ CL_Shutdown */ void CL_Shutdown (void) { + CL_Screen_Shutdown(); CL_Particles_Shutdown(); CL_Parse_Shutdown(); diff --git a/cl_screen.c b/cl_screen.c index 4768f4b7..b338efe5 100644 --- a/cl_screen.c +++ b/cl_screen.c @@ -30,6 +30,8 @@ cvar_t scr_screenshot_jpeg_quality = {CVAR_SAVE, "scr_screenshot_jpeg_quality"," cvar_t scr_screenshot_gammaboost = {CVAR_SAVE, "scr_screenshot_gammaboost","1", "gamma correction on saved screenshots and videos, 1.0 saves unmodified images"}; // scr_screenshot_name is defined in fs.c cvar_t cl_capturevideo = {0, "cl_capturevideo", "0", "enables saving of video to a .avi file using uncompressed I420 colorspace and PCM audio, note that scr_screenshot_gammaboost affects the brightness of the output)"}; +cvar_t cl_capturevideo_width = {0, "cl_capturevideo_width", "0", "scales all frames to this resolution before saving the video"}; +cvar_t cl_capturevideo_height = {0, "cl_capturevideo_height", "0", "scales all frames to this resolution before saving the video"}; cvar_t cl_capturevideo_realtime = {0, "cl_capturevideo_realtime", "0", "causes video saving to operate in realtime (mostly useful while playing, not while capturing demos), this can produce a much lower quality video due to poor sound/video sync and will abort saving if your machine stalls for over 1 second"}; cvar_t cl_capturevideo_fps = {0, "cl_capturevideo_fps", "30", "how many frames per second to save (29.97 for NTSC, 30 for typical PC video, 15 can be useful)"}; cvar_t cl_capturevideo_number = {CVAR_SAVE, "cl_capturevideo_number", "1", "number to append to video filename, incremented each time a capture begins"}; @@ -719,6 +721,12 @@ void SCR_SizeDown_f (void) Cvar_SetValue ("viewsize",scr_viewsize.value-10); } +void SCR_CaptureVideo_EndVideo(void); +void CL_Screen_Shutdown(void) +{ + SCR_CaptureVideo_EndVideo(); +} + void CL_Screen_Init(void) { Cvar_RegisterVariable (&scr_fov); @@ -740,6 +748,8 @@ void CL_Screen_Init(void) Cvar_RegisterVariable (&scr_screenshot_jpeg_quality); Cvar_RegisterVariable (&scr_screenshot_gammaboost); Cvar_RegisterVariable (&cl_capturevideo); + Cvar_RegisterVariable (&cl_capturevideo_width); + Cvar_RegisterVariable (&cl_capturevideo_height); Cvar_RegisterVariable (&cl_capturevideo_realtime); Cvar_RegisterVariable (&cl_capturevideo_fps); Cvar_RegisterVariable (&cl_capturevideo_number); @@ -930,49 +940,110 @@ static void SCR_CaptureVideo_RIFF_IndexEntry(const char *chunkfourcc, int chunks MSG_WriteLong(&cls.capturevideo.riffindexbuffer, chunksize); } -static void SCR_CaptureVideo_RIFF_Finish(void) +static void SCR_CaptureVideo_RIFF_Finish(qboolean final) { // close the "movi" list SCR_CaptureVideo_RIFF_Pop(); // write the idx1 chunk that we've been building while saving the frames - SCR_CaptureVideo_RIFF_Push("idx1", NULL); - SCR_CaptureVideo_RIFF_WriteBytes(cls.capturevideo.riffindexbuffer.data, cls.capturevideo.riffindexbuffer.cursize); - SCR_CaptureVideo_RIFF_Pop(); + if(final && cls.capturevideo.videofile_firstchunkframes_offset) + // TODO replace index creating by OpenDML ix##/##ix/indx chunk so it works for more than one AVI part too + { + SCR_CaptureVideo_RIFF_Push("idx1", NULL); + SCR_CaptureVideo_RIFF_WriteBytes(cls.capturevideo.riffindexbuffer.data, cls.capturevideo.riffindexbuffer.cursize); + SCR_CaptureVideo_RIFF_Pop(); + } cls.capturevideo.riffindexbuffer.cursize = 0; // pop the RIFF chunk itself while (cls.capturevideo.riffstacklevel > 0) SCR_CaptureVideo_RIFF_Pop(); SCR_CaptureVideo_RIFF_Flush(); + if(cls.capturevideo.videofile_firstchunkframes_offset) + { + Con_DPrintf("Finishing first chunk (%d frames)\n", cls.capturevideo.frame); + FS_Seek(cls.capturevideo.videofile, cls.capturevideo.videofile_firstchunkframes_offset, SEEK_SET); + SCR_CaptureVideo_RIFF_Write32(cls.capturevideo.frame); + SCR_CaptureVideo_RIFF_Flush(); + FS_Seek(cls.capturevideo.videofile, 0, SEEK_END); + cls.capturevideo.videofile_firstchunkframes_offset = 0; + } + else + Con_DPrintf("Finishing another chunk (%d frames)\n", cls.capturevideo.frame); } static void SCR_CaptureVideo_RIFF_OverflowCheck(int framesize) { - fs_offset_t cursize; + fs_offset_t cursize, curfilesize; if (cls.capturevideo.riffstacklevel != 2) Sys_Error("SCR_CaptureVideo_RIFF_OverflowCheck: chunk stack leakage!\n"); // check where we are in the file SCR_CaptureVideo_RIFF_Flush(); cursize = SCR_CaptureVideo_RIFF_GetPosition() - cls.capturevideo.riffstackstartoffset[0]; + curfilesize = SCR_CaptureVideo_RIFF_GetPosition(); + // if this would overflow the windows limit of 1GB per RIFF chunk, we need // to close the current RIFF chunk and open another for future frames if (8 + cursize + framesize + cls.capturevideo.riffindexbuffer.cursize + 8 > 1<<30) { - SCR_CaptureVideo_RIFF_Finish(); + SCR_CaptureVideo_RIFF_Finish(false); // begin a new 1GB extended section of the AVI SCR_CaptureVideo_RIFF_Push("RIFF", "AVIX"); SCR_CaptureVideo_RIFF_Push("LIST", "movi"); } } +static void FindFraction(double val, int *num, int *denom, int denomMax) +{ + int i; + double bestdiff; + // initialize + bestdiff = fabs(val); + *num = 0; + *denom = 1; + + for(i = 1; i <= denomMax; ++i) + { + int inum = floor(0.5 + val * i); + double diff = fabs(val - inum / (double)i); + if(diff < bestdiff) + { + bestdiff = diff; + *num = inum; + *denom = i; + } + } +} + void SCR_CaptureVideo_BeginVideo(void) { - double gamma, g; - int width = vid.width, height = vid.height, x; + double gamma, g, aspect; + int width = cl_capturevideo_width.integer, height = cl_capturevideo_height.integer; + int n, d; unsigned int i; if (cls.capturevideo.active) return; memset(&cls.capturevideo, 0, sizeof(cls.capturevideo)); // soundrate is figured out on the first SoundFrame + + if(width == 0 && height != 0) + width = (int) (height * (double)vid.width / ((double)vid.height * vid_pixelheight.value)); // keep aspect + if(width != 0 && height == 0) + height = (int) (width * ((double)vid.height * vid_pixelheight.value) / (double)vid.width); // keep aspect + + if(width < 2 || width > vid.width) // can't scale up + width = vid.width; + if(height < 2 || height > vid.height) // can't scale up + height = vid.height; + + aspect = vid.width / (vid.height * vid_pixelheight.value); + + // ensure it's all even; if not, scale down a little + if(width % 1) + --width; + if(height % 1) + --height; + + cls.capturevideo.width = width; + cls.capturevideo.height = height; cls.capturevideo.active = true; cls.capturevideo.starttime = realtime; cls.capturevideo.framerate = bound(1, cl_capturevideo_fps.value, 1000); @@ -980,7 +1051,8 @@ void SCR_CaptureVideo_BeginVideo(void) cls.capturevideo.frame = 0; cls.capturevideo.soundsampleframe = 0; cls.capturevideo.realtime = cl_capturevideo_realtime.integer != 0; - cls.capturevideo.buffer = (unsigned char *)Mem_Alloc(tempmempool, vid.width * vid.height * (3+3+3) + 18); + cls.capturevideo.screenbuffer = (unsigned char *)Mem_Alloc(tempmempool, vid.width * vid.height * 3); + cls.capturevideo.outbuffer = (unsigned char *)Mem_Alloc(tempmempool, width * height * (3+3+3) + 18); gamma = 1.0/scr_screenshot_gammaboost.value; dpsnprintf(cls.capturevideo.basename, sizeof(cls.capturevideo.basename), "video/dpvideo%03i", cl_capturevideo_number.integer); Cvar_SetValueQuick(&cl_capturevideo_number, cl_capturevideo_number.integer + 1); @@ -1040,7 +1112,7 @@ Cr = R * .500 + G * -.419 + B * -.0813 + 128.; SCR_CaptureVideo_RIFF_Write32(0); // max bytes per second SCR_CaptureVideo_RIFF_Write32(0); // padding granularity SCR_CaptureVideo_RIFF_Write32(0x910); // flags (AVIF_HASINDEX | AVIF_ISINTERLEAVED | AVIF_TRUSTCKTYPE) - cls.capturevideo.videofile_totalframes_offset1 = SCR_CaptureVideo_RIFF_GetPosition(); + cls.capturevideo.videofile_firstchunkframes_offset = SCR_CaptureVideo_RIFF_GetPosition(); SCR_CaptureVideo_RIFF_Write32(0); // total frames SCR_CaptureVideo_RIFF_Write32(0); // initial frames if (cls.capturevideo.soundrate) @@ -1064,13 +1136,11 @@ Cr = R * .500 + G * -.419 + B * -.0813 + 128.; SCR_CaptureVideo_RIFF_Write16(0); // language SCR_CaptureVideo_RIFF_Write32(0); // initial frames // find an ideal divisor for the framerate - for (x = 1;x < 1000;x++) - if (cls.capturevideo.framerate * x == floor(cls.capturevideo.framerate * x)) - break; - SCR_CaptureVideo_RIFF_Write32(x); // samples/second divisor - SCR_CaptureVideo_RIFF_Write32((int)(cls.capturevideo.framerate * x)); // samples/second multiplied by divisor + FindFraction(cls.capturevideo.framerate, &n, &d, 1000); + SCR_CaptureVideo_RIFF_Write32(d); // samples/second divisor + SCR_CaptureVideo_RIFF_Write32(n); // samples/second multiplied by divisor SCR_CaptureVideo_RIFF_Write32(0); // start - cls.capturevideo.videofile_totalframes_offset2 = SCR_CaptureVideo_RIFF_GetPosition(); + cls.capturevideo.videofile_totalframes_offset1 = SCR_CaptureVideo_RIFF_GetPosition(); SCR_CaptureVideo_RIFF_Write32(0); // length SCR_CaptureVideo_RIFF_Write32(width*height+(width/2)*(height/2)*2); // suggested buffer size SCR_CaptureVideo_RIFF_Write32(0); // quality @@ -1094,6 +1164,27 @@ Cr = R * .500 + G * -.419 + B * -.0813 + 128.; SCR_CaptureVideo_RIFF_Write32(0); // color used SCR_CaptureVideo_RIFF_Write32(0); // color important SCR_CaptureVideo_RIFF_Pop(); + // extended format (aspect!) + SCR_CaptureVideo_RIFF_Push("vprp", NULL); + SCR_CaptureVideo_RIFF_Write32(0); // VideoFormatToken + SCR_CaptureVideo_RIFF_Write32(0); // VideoStandard + SCR_CaptureVideo_RIFF_Write32(cls.capturevideo.framerate); // dwVerticalRefreshRate (bogus) + SCR_CaptureVideo_RIFF_Write32(width); // dwHTotalInT + SCR_CaptureVideo_RIFF_Write32(height); // dwVTotalInLines + FindFraction(aspect, &n, &d, 1000); + SCR_CaptureVideo_RIFF_Write32((n << 16) | d); // dwFrameAspectRatio // TODO a word + SCR_CaptureVideo_RIFF_Write32(width); // dwFrameWidthInPixels + SCR_CaptureVideo_RIFF_Write32(height); // dwFrameHeightInLines + SCR_CaptureVideo_RIFF_Write32(1); // nFieldPerFrame + SCR_CaptureVideo_RIFF_Write32(width); // CompressedBMWidth + SCR_CaptureVideo_RIFF_Write32(height); // CompressedBMHeight + SCR_CaptureVideo_RIFF_Write32(width); // ValidBMHeight + SCR_CaptureVideo_RIFF_Write32(height); // ValidBMWidth + SCR_CaptureVideo_RIFF_Write32(0); // ValidBMXOffset + SCR_CaptureVideo_RIFF_Write32(0); // ValidBMYOffset + SCR_CaptureVideo_RIFF_Write32(0); // ValidBMXOffsetInT + SCR_CaptureVideo_RIFF_Write32(0); // ValidBMYValidStartLine + SCR_CaptureVideo_RIFF_Pop(); SCR_CaptureVideo_RIFF_Pop(); if (cls.capturevideo.soundrate) { @@ -1130,6 +1221,15 @@ Cr = R * .500 + G * -.419 + B * -.0813 + 128.; SCR_CaptureVideo_RIFF_Pop(); SCR_CaptureVideo_RIFF_Pop(); } + + // extended header (for total #frames) + SCR_CaptureVideo_RIFF_Push("LIST", "odml"); + SCR_CaptureVideo_RIFF_Push("dmlh", NULL); + cls.capturevideo.videofile_totalframes_offset2 = SCR_CaptureVideo_RIFF_GetPosition(); + SCR_CaptureVideo_RIFF_Write32(0); + SCR_CaptureVideo_RIFF_Pop(); + SCR_CaptureVideo_RIFF_Pop(); + // close the AVI header list SCR_CaptureVideo_RIFF_Pop(); // software that produced this AVI video file @@ -1179,8 +1279,9 @@ void SCR_CaptureVideo_EndVideo(void) { case CAPTUREVIDEOFORMAT_AVI_I420: // close any open chunks - SCR_CaptureVideo_RIFF_Finish(); + SCR_CaptureVideo_RIFF_Finish(true); // go back and fix the video frames and audio samples fields + Con_DPrintf("Finishing capture (%d frames, %d audio frames)\n", cls.capturevideo.frame, cls.capturevideo.soundsampleframe); FS_Seek(cls.capturevideo.videofile, cls.capturevideo.videofile_totalframes_offset1, SEEK_SET); SCR_CaptureVideo_RIFF_Write32(cls.capturevideo.frame); SCR_CaptureVideo_RIFF_Flush(); @@ -1201,10 +1302,16 @@ void SCR_CaptureVideo_EndVideo(void) cls.capturevideo.videofile = NULL; } - if (cls.capturevideo.buffer) + if (cls.capturevideo.screenbuffer) { - Mem_Free (cls.capturevideo.buffer); - cls.capturevideo.buffer = NULL; + Mem_Free (cls.capturevideo.screenbuffer); + cls.capturevideo.screenbuffer = NULL; + } + + if (cls.capturevideo.outbuffer) + { + Mem_Free (cls.capturevideo.outbuffer); + cls.capturevideo.outbuffer = NULL; } if (cls.capturevideo.riffindexbuffer.data) @@ -1259,9 +1366,55 @@ void SCR_CaptureVideo_ConvertFrame_RGB_to_I420_flip(int width, int height, unsig } } +static void SCR_ScaleDown(unsigned char *in, int inw, int inh, unsigned char *out, int outw, int outh) +{ + // TODO optimize this function + + int x, y; + float area; + + // memcpy is faster than me + if(inw == outw && inh == outh) + { + memcpy(out, in, 3 * inw * inh); + return; + } + + // otherwise: a box filter + area = (float)outw * (float)outh / (float)inw / (float)inh; + for(y = 0; y < outh; ++y) + { + float iny0 = y / (float)outh * inh; int iny0_i = floor(iny0); + float iny1 = (y+1) / (float)outh * inh; int iny1_i = ceil(iny1); + for(x = 0; x < outw; ++x) + { + float inx0 = x / (float)outw * inw; int inx0_i = floor(inx0); + float inx1 = (x+1) / (float)outw * inw; int inx1_i = ceil(inx1); + float r = 0, g = 0, b = 0; + int xx, yy; + + for(yy = iny0_i; yy < iny1_i; ++yy) + { + float ya = min(yy+1, iny1) - max(iny0, yy); + for(xx = inx0_i; xx < inx1_i; ++xx) + { + float a = ya * (min(xx+1, inx1) - max(inx0, xx)); + r += a * in[3*(xx + inw * yy)+0]; + g += a * in[3*(xx + inw * yy)+1]; + b += a * in[3*(xx + inw * yy)+2]; + } + } + + out[3*(x + outw * y)+0] = r * area; + out[3*(x + outw * y)+1] = g * area; + out[3*(x + outw * y)+2] = b * area; + } + } +} + qboolean SCR_CaptureVideo_VideoFrame(int newframenum) { - int x = 0, y = 0, width = vid.width, height = vid.height; + int x = 0, y = 0, width = cls.capturevideo.width, height = cls.capturevideo.height; unsigned char *in, *out; CHECKGLERROR //return SCR_ScreenShot(filename, cls.capturevideo.buffer, cls.capturevideo.buffer + vid.width * vid.height * 3, cls.capturevideo.buffer + vid.width * vid.height * 6, 0, 0, vid.width, vid.height, false, false, false, jpeg, true); @@ -1273,9 +1426,10 @@ qboolean SCR_CaptureVideo_VideoFrame(int newframenum) if (!cls.capturevideo.videofile) return false; // FIXME: width/height must be multiple of 2, enforce this? - qglReadPixels (x, y, width, height, GL_RGB, GL_UNSIGNED_BYTE, cls.capturevideo.buffer);CHECKGLERROR - in = cls.capturevideo.buffer; - out = cls.capturevideo.buffer + width*height*3; + qglReadPixels (x, y, vid.width, vid.height, GL_RGB, GL_UNSIGNED_BYTE, cls.capturevideo.screenbuffer);CHECKGLERROR + SCR_ScaleDown (cls.capturevideo.screenbuffer, vid.width, vid.height, cls.capturevideo.outbuffer, width, height); + in = cls.capturevideo.outbuffer; + out = cls.capturevideo.outbuffer + width*height*3; SCR_CaptureVideo_ConvertFrame_RGB_to_I420_flip(width, height, in, out); x = width*height+(width/2)*(height/2)*2; SCR_CaptureVideo_RIFF_OverflowCheck(8 + x); diff --git a/cl_screen.h b/cl_screen.h index 471a3645..9083ee31 100644 --- a/cl_screen.h +++ b/cl_screen.h @@ -17,6 +17,7 @@ extern cvar_t scr_screenshot_name; void CL_Screen_NewMap(void); void CL_Screen_Init(void); +void CL_Screen_Shutdown(void); void CL_UpdateScreen(void); #endif diff --git a/client.h b/client.h index 76fce5bf..0b16a62d 100644 --- a/client.h +++ b/client.h @@ -439,6 +439,7 @@ typedef struct capturevideostate_s double starttime; double framerate; // for AVI saving some values have to be written after capture ends + fs_offset_t videofile_firstchunkframes_offset; fs_offset_t videofile_totalframes_offset1; fs_offset_t videofile_totalframes_offset2; fs_offset_t videofile_totalsampleframes_offset; @@ -450,7 +451,8 @@ typedef struct capturevideostate_s int soundrate; int frame; int soundsampleframe; // for AVI saving - unsigned char *buffer; + unsigned char *screenbuffer; + unsigned char *outbuffer; sizebuf_t riffbuffer; unsigned char riffbufferdata[128]; // note: riffindex buffer has an allocated ->data member, not static like most! @@ -460,6 +462,7 @@ typedef struct capturevideostate_s short rgbtoyuvscaletable[3][3][256]; unsigned char yuvnormalizetable[3][256]; char basename[64]; + int width, height; } capturevideostate_t; diff --git a/host.c b/host.c index 66f4fa76..812a2cb6 100644 --- a/host.c +++ b/host.c @@ -222,7 +222,7 @@ void Host_SaveConfig_f(void) // dedicated servers initialize the host but don't parse and set the // config.cfg cvars // LordHavoc: don't save a config if it crashed in startup - if (host_framecount >= 3 && cls.state != ca_dedicated && !COM_CheckParm("-benchmark")) + if (host_framecount >= 3 && cls.state != ca_dedicated && !COM_CheckParm("-benchmark") && !COM_CheckParm("-capturedemo")) { f = FS_Open ("config.cfg", "wb", false, false); if (!f) @@ -1005,6 +1005,15 @@ static void Host_Init (void) Cbuf_Execute(); } +// COMMANDLINEOPTION: Client: -capturedemo captures a playdemo and quits + i = COM_CheckParm("-capturedemo"); + if (i && i + 1 < com_argc) + if (!sv.active && !cls.demoplayback && !cls.connect_trying) + { + Cbuf_AddText(va("playdemo %s\ncl_capturevideo 1\n", com_argv[i + 1])); + Cbuf_Execute(); + } + if (cls.state == ca_dedicated || COM_CheckParm("-listen")) if (!sv.active && !cls.demoplayback && !cls.connect_trying) { @@ -1044,6 +1053,11 @@ void Host_Shutdown(void) Con_Print("recursive shutdown\n"); return; } + if (setjmp(host_abortframe)) + { + Con_Print("aborted the quitting frame?!?\n"); + return; + } isdown = true; // be quiet while shutting down diff --git a/host_cmd.c b/host_cmd.c index 7875bffa..eb23e394 100644 --- a/host_cmd.c +++ b/host_cmd.c @@ -30,6 +30,8 @@ cvar_t skin = {CVAR_USERINFO | CVAR_SAVE, "skin", "", "QW player skin name (exam cvar_t noaim = {CVAR_USERINFO | CVAR_SAVE, "noaim", "1", "QW option to disable vertical autoaim"}; qboolean allowcheats = false; +extern qboolean host_shuttingdown; + /* ================== Host_Quit_f @@ -38,7 +40,10 @@ Host_Quit_f void Host_Quit_f (void) { - Sys_Quit (0); + if(host_shuttingdown) + Con_Printf("shutting down already!\n"); + else + Sys_Quit (0); } @@ -1910,7 +1915,7 @@ void Host_Startdemos_f (void) { int i, c; - if (cls.state == ca_dedicated || COM_CheckParm("-listen") || COM_CheckParm("-benchmark") || COM_CheckParm("-demo")) + if (cls.state == ca_dedicated || COM_CheckParm("-listen") || COM_CheckParm("-benchmark") || COM_CheckParm("-demo") || COM_CheckParm("-capturedemo")) return; c = Cmd_Argc() - 1; -- 2.39.2