From 730ac5e3e90b08e10db5dbd975f6a88e7c7cb165 Mon Sep 17 00:00:00 2001 From: James R Date: Fri, 10 Feb 2023 07:08:19 -0800 Subject: [PATCH 01/38] clang-format: constructor member initiailizer list breaks with trailing colon/commas Compare before / after: A() B() : : a(0) a(0), , b(1) b(1) --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 15b53c893..085cde61f 100644 --- a/.clang-format +++ b/.clang-format @@ -20,7 +20,7 @@ AlwaysBreakTemplateDeclarations: Yes BinPackArguments: false BinPackParameters: false BreakBeforeBraces: Allman # Always break before braces, to match existing SRB2 code -BreakConstructorInitializers: BeforeComma +BreakConstructorInitializers: AfterColon CompactNamespaces: true ConstructorInitializerAllOnOneLineOrOnePerLine: true Cpp11BracedListStyle: true From 6e79e0b92edab63e498d3a5c20d8db51d1234dca Mon Sep 17 00:00:00 2001 From: James R Date: Fri, 10 Feb 2023 07:12:38 -0800 Subject: [PATCH 02/38] clang-format: really avoid return type alone on line Should fix some instances of very long lines doing the following: void fn( ...really long parameter list... ) --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index 085cde61f..e1599848d 100644 --- a/.clang-format +++ b/.clang-format @@ -55,3 +55,4 @@ SpacesInConditionalStatement: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false +PenaltyReturnTypeOnItsOwnLine: 1000 From f9f4d56850926f9f14a6b7a72d91db6281a3b764 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 23 Feb 2023 15:56:37 +0000 Subject: [PATCH 03/38] m_cond.c/h: Copyright notice update Intended to do this in my previous `unlockables-undefeatable` branch but forgot --- src/m_cond.c | 1 + src/m_cond.h | 1 + 2 files changed, 2 insertions(+) diff --git a/src/m_cond.c b/src/m_cond.c index abc228d3a..ffcbca21b 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -1,5 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Vivian "toaster" Grannell. // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh. // Copyright (C) 2012-2020 by Sonic Team Junior. // diff --git a/src/m_cond.h b/src/m_cond.h index 735894c71..eb8ebe7c5 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -1,5 +1,6 @@ // SONIC ROBO BLAST 2 //----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Vivian "toaster" Grannell. // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh. // Copyright (C) 2012-2020 by Sonic Team Junior. // From d66a4cff8182536307df2d11aa12a9a76d39b8c7 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 23 Feb 2023 16:55:36 +0000 Subject: [PATCH 04/38] M_DrawChallenges: Scrolling question mark background --- src/k_menudraw.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 32476585a..100d1dcc9 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4828,8 +4828,25 @@ void M_DrawChallenges(void) INT16 offset; { - patch_t *bg = W_CachePatchName("BGUNLCK2", PU_CACHE); +#define questionslow 4 // slows down the scroll by this factor +#define questionloop (questionslow*100) // modulo + INT32 questionoffset = (challengesmenu.ticker % questionloop); + patch_t *bg = W_CachePatchName("BGUNLCKG", PU_CACHE); + patch_t *qm = W_CachePatchName("BGUNLSC", PU_CACHE); + + // Background gradient V_DrawFixedPatch(0, 0, FRACUNIT, 0, bg, NULL); + + // Scrolling question mark overlay + V_DrawFixedPatch( + -((160 + questionoffset)*FRACUNIT)/questionslow, + -(4*FRACUNIT) - (245*(FixedDiv((questionloop - questionoffset)*FRACUNIT, questionloop*FRACUNIT))), + FRACUNIT, + V_MODULATE, + qm, + NULL); +#undef questionslow +#undef questionloop } if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL) From d17525fac78488d86f90e989c6c35bfc2d9b139d Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 23 Feb 2023 17:24:11 +0000 Subject: [PATCH 05/38] M_DrawChallenges: reimplement dark areas for contrast/readability Now uses V_DrawFadeFill and MENUHINT patch directly. --- src/k_menudraw.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 100d1dcc9..b3b750f45 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4819,6 +4819,8 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) } } +#define challengetransparentstrength 8 + void M_DrawChallenges(void) { INT32 x = currentMenu->x, explodex, selectx; @@ -4855,6 +4857,8 @@ void M_DrawChallenges(void) goto challengedesc; } + V_DrawFadeFill(0, y-2, BASEVIDWIDTH, 90, 0, 31, challengetransparentstrength); + x -= 16; x += challengesmenu.offset; @@ -4939,6 +4943,12 @@ challengedesc: { y = 120; + V_DrawScaledPatch(0, y, + (10-challengetransparentstrength)< Date: Thu, 23 Feb 2023 17:57:01 +0000 Subject: [PATCH 06/38] Challenge Grid: set height to 4 - In anticipation of next steps of menu redesign - g_game.c: Increment GD_VERSIONMINOR and burn old challenge grid data --- src/g_game.c | 42 ++++++++++++++++++++++++++++++------------ src/m_cond.h | 2 +- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/g_game.c b/src/g_game.c index ecf088eaf..189f22fa4 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4354,7 +4354,7 @@ void G_LoadGameSettings(void) } #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 0 // Change every format update +#define GD_VERSIONMINOR 1 // Change every format update // G_LoadGameData // Loads the main data file, which stores information such as emblems found, etc. @@ -4364,6 +4364,7 @@ void G_LoadGameData(void) UINT32 versionID; UINT8 versionMinor; UINT8 rtemp; + boolean gridunusable = false; savebuffer_t save = {0}; //For records @@ -4418,6 +4419,10 @@ void G_LoadGameData(void) P_SaveBufferFree(&save); I_Error("Game data is from the future! (expected %d, got %d)", GD_VERSIONMINOR, versionMinor); } + if (versionMinor == 0) + { + gridunusable = true; + } gamedata->totalplaytime = READUINT32(save.p); gamedata->matchesplayed = READUINT32(save.p); @@ -4464,21 +4469,34 @@ void G_LoadGameData(void) i += j; } - gamedata->challengegridwidth = READUINT16(save.p); - Z_Free(gamedata->challengegrid); - if (gamedata->challengegridwidth) + if (gridunusable) { - gamedata->challengegrid = Z_Malloc( - (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), - PU_STATIC, NULL); - for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) - { - gamedata->challengegrid[i] = READUINT8(save.p); - } + UINT16 burn = READUINT16(save.p); // Previous challengegridwidth + UINT8 height = (versionMinor > 0) ? CHALLENGEGRIDHEIGHT : 5; + save.p += (burn * height * sizeof(UINT8)); // Step over previous grid data + + gamedata->challengegridwidth = 0; + Z_Free(gamedata->challengegrid); + gamedata->challengegrid = NULL; } else { - gamedata->challengegrid = NULL; + gamedata->challengegridwidth = READUINT16(save.p); + Z_Free(gamedata->challengegrid); + if (gamedata->challengegridwidth) + { + gamedata->challengegrid = Z_Malloc( + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), + PU_STATIC, NULL); + for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++) + { + gamedata->challengegrid[i] = READUINT8(save.p); + } + } + else + { + gamedata->challengegrid = NULL; + } } gamedata->timesBeaten = READUINT32(save.p); diff --git a/src/m_cond.h b/src/m_cond.h index eb8ebe7c5..9439cfe86 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -139,7 +139,7 @@ typedef enum #define MAXEMBLEMS 512 #define MAXUNLOCKABLES MAXCONDITIONSETS -#define CHALLENGEGRIDHEIGHT 5 +#define CHALLENGEGRIDHEIGHT 4 #ifdef DEVELOP #define CHALLENGEGRIDLOOPWIDTH 3 #else From 287540039761ad0d66d5b0deee91cd6e2a065b3f Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 23 Feb 2023 21:39:55 +0000 Subject: [PATCH 07/38] Challenges Grid: New tile borders + selection reticule M_DrawChallenges: Adjust all offsets to new 22x22 bordered system (from 16x16) M_DrawChallengeTile: Border, colorization, and reticule --- src/k_hud.h | 1 - src/k_menudraw.c | 124 +++++++++++++++++++++++++++++++---------------- 2 files changed, 82 insertions(+), 43 deletions(-) diff --git a/src/k_hud.h b/src/k_hud.h index b80760c2c..e9b0603f2 100644 --- a/src/k_hud.h +++ b/src/k_hud.h @@ -46,7 +46,6 @@ void K_DrawMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, UINT16 map, void K_DrawLikeMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, patch_t *patch, UINT8 *colormap); void K_drawTargetHUD(const vector3_t *origin, player_t *player); -extern patch_t *kp_facehighlight[8]; extern patch_t *kp_capsuletarget_arrow[2][2]; extern patch_t *kp_capsuletarget_icon[2]; extern patch_t *kp_capsuletarget_far[2]; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index b3b750f45..dc37e7c70 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4519,7 +4519,7 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili UINT8 *colormap = NULL; fixed_t siz; UINT8 id, num; - UINT32 edgelength; + boolean unlockedyet; id = (i * CHALLENGEGRIDHEIGHT) + j; num = gamedata->challengegrid[id]; @@ -4533,14 +4533,36 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili // Okay, this is what we want to draw. ref = &unlockables[num]; - edgelength = (ref->majorunlock ? 30 : 14); + unlockedyet = !((gamedata->unlocked[num] == false) + || (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME)); - // ...unless we simply aren't unlocked yet. - if ((gamedata->unlocked[num] == false) - || (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME)) + pat = W_CachePatchName( + (ref->majorunlock ? "UN_BORDB" : "UN_BORDA"), + PU_CACHE); + + if (!unlockedyet) + { + UINT16 col = (challengesmenu.extradata[id] == CHE_HINT) ? SKINCOLOR_BLUE : SKINCOLOR_BLACK; + colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); + } + else + { + colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE); + } + + V_DrawFixedPatch( + x*FRACUNIT, y*FRACUNIT, + FRACUNIT, + 0, pat, + colormap + ); + + pat = missingpat; + colormap = NULL; + + // If we aren't unlocked yet, return early. + if (!unlockedyet) { - V_DrawFill(x+1, y+1, edgelength, edgelength, - ((challengesmenu.extradata[id] == CHE_HINT) ? 132 : 11)); goto drawborder; } @@ -4588,22 +4610,27 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili } siz = (SHORT(pat->width) << FRACBITS); - siz = FixedDiv(((ref->majorunlock) ? 32 : 16) << FRACBITS, siz); - V_SetClipRect( - (x+1) << FRACBITS, (y+1) << FRACBITS, - edgelength << FRACBITS, edgelength << FRACBITS, - 0 - ); - - V_DrawFixedPatch( - x*FRACUNIT, y*FRACUNIT, - siz, - 0, pat, - colormap - ); - - V_ClearClipRect(); + if (!siz) + ; // prevent div/0 + else if (ref->majorunlock) + { + V_DrawFixedPatch( + (x + 5)*FRACUNIT, (y + 5)*FRACUNIT, + FixedDiv(32 << FRACBITS, siz), + 0, pat, + colormap + ); + } + else + { + V_DrawFixedPatch( + (x + 2)*FRACUNIT, (y + 2)*FRACUNIT, + FixedDiv(16 << FRACBITS, siz), + 0, pat, + colormap + ); + } drawborder: if (!hili) @@ -4611,12 +4638,23 @@ drawborder: return; } - V_DrawFixedPatch( - x*FRACUNIT, y*FRACUNIT, - ((ref != NULL && ref->majorunlock) ? FRACUNIT*2 : FRACUNIT), - 0, kp_facehighlight[(challengesmenu.ticker / 4) % 8], - NULL - ); + { + boolean maj = (ref != NULL && ref->majorunlock); + char buffer[9]; + sprintf(buffer, "UN_RETA1"); + buffer[6] = maj ? 'B' : 'A'; + buffer[7] = (skullAnimCounter/5) ? '2' : '1'; + pat = W_CachePatchName(buffer, PU_CACHE); + + colormap = R_GetTranslationColormap(TC_DEFAULT, cv_playercolor[0].value, GTC_MENUCACHE); + + V_DrawFixedPatch( + x*FRACUNIT, y*FRACUNIT, + FRACUNIT, + 0, pat, + colormap + ); + } } static void M_DrawChallengePreview(INT32 x, INT32 y) @@ -4820,6 +4858,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) } #define challengetransparentstrength 8 +#define challengesgridstep 22 void M_DrawChallenges(void) { @@ -4859,41 +4898,41 @@ void M_DrawChallenges(void) V_DrawFadeFill(0, y-2, BASEVIDWIDTH, 90, 0, 31, challengetransparentstrength); - x -= 16; + x -= (challengesgridstep-1); x += challengesmenu.offset; if (challengegridloops) { if (!challengesmenu.col && challengesmenu.hilix) - x -= gamedata->challengegridwidth*16; + x -= gamedata->challengegridwidth*challengesgridstep; i = challengesmenu.col + challengesmenu.focusx; - explodex = x - (i*16); + explodex = x - (i*challengesgridstep); - while (x < BASEVIDWIDTH-16) + while (x < BASEVIDWIDTH-challengesgridstep) { i = (i + 1) % gamedata->challengegridwidth; - x += 16; + x += challengesgridstep; } } else { if (gamedata->challengegridwidth & 1) - x += 8; + x += (challengesgridstep/2); i = gamedata->challengegridwidth-1; - explodex = x - (i*16)/2; - x += (i*16)/2; + explodex = x - (i*challengesgridstep)/2; + x += (i*challengesgridstep)/2; } - selectx = explodex + (challengesmenu.hilix*16); + selectx = explodex + (challengesmenu.hilix*challengesgridstep); - while (i >= 0 && x >= -32) + while (i >= 0 && x >= -(challengesgridstep*2)) { - y = currentMenu->y-16; + y = currentMenu->y-challengesgridstep; for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) { - y += 16; + y += challengesgridstep; if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j] & CHE_DONTDRAW) { @@ -4908,7 +4947,7 @@ void M_DrawChallenges(void) M_DrawChallengeTile(i, j, x, y, false); } - x -= 16; + x -= challengesgridstep; i--; if (challengegridloops && i < 0) { @@ -4924,7 +4963,7 @@ void M_DrawChallenges(void) challengesmenu.hilix, challengesmenu.hiliy, selectx, - currentMenu->y + (challengesmenu.hiliy*16), + currentMenu->y + (challengesmenu.hiliy*challengesgridstep), true); M_DrawCharSelectExplosions(false, explodex, currentMenu->y); @@ -4989,6 +5028,7 @@ challengedesc: } #undef challengetransparentstrength +#undef challengesgridstep // Statistics menu From 2164121ce6a209ee4d2dd6ecff19c1bab40e8392 Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 23 Feb 2023 23:38:06 +0000 Subject: [PATCH 08/38] M_DrawChallengeTile: Have different appearance for hovered over tile (current behaviour) and category information --- src/k_menudraw.c | 152 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 122 insertions(+), 30 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index dc37e7c70..d617e87cd 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4516,10 +4516,11 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili { unlockable_t *ref = NULL; patch_t *pat = missingpat; - UINT8 *colormap = NULL; + UINT8 *colormap = NULL, *bgmap = NULL; fixed_t siz; UINT8 id, num; boolean unlockedyet; + boolean categoryside; id = (i * CHALLENGEGRIDHEIGHT) + j; num = gamedata->challengegrid[id]; @@ -4543,22 +4544,21 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili if (!unlockedyet) { UINT16 col = (challengesmenu.extradata[id] == CHE_HINT) ? SKINCOLOR_BLUE : SKINCOLOR_BLACK; - colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); + bgmap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); } else { - colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE); + bgmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE); } V_DrawFixedPatch( x*FRACUNIT, y*FRACUNIT, FRACUNIT, 0, pat, - colormap + bgmap ); pat = missingpat; - colormap = NULL; // If we aren't unlocked yet, return early. if (!unlockedyet) @@ -4566,7 +4566,54 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili goto drawborder; } - if (ref->icon != NULL && ref->icon[0]) + categoryside = !hili; // temporary + + if (categoryside) + { + char categoryid = '8'; + colormap = bgmap; + switch (ref->type) + { + case SECRET_SKIN: + categoryid = '1'; + break; + case SECRET_FOLLOWER: + categoryid = '2'; + break; + /*case SECRET_COLOR: + categoryid = '3'; + break;*/ + case SECRET_CUP: + categoryid = '4'; + break; + //case SECRET_MASTERBOTS: + case SECRET_HARDSPEED: + case SECRET_ENCORE: + categoryid = '5'; + break; + case SECRET_ALTTITLE: + case SECRET_SOUNDTEST: + categoryid = '6'; + break; + case SECRET_TIMEATTACK: + case SECRET_BREAKTHECAPSULES: + case SECRET_SPECIALATTACK: + categoryid = '7'; + break; + } + pat = W_CachePatchName(va("UN_RR0%c%c", + categoryid, + (ref->majorunlock) ? 'B' : 'A'), + PU_CACHE); + if (pat == missingpat) + { + pat = W_CachePatchName(va("UN_RR0%c%c", + categoryid, + (ref->majorunlock) ? 'A' : 'B'), + PU_CACHE); + } + } + else if (ref->icon != NULL && ref->icon[0]) { pat = W_CachePatchName(ref->icon, PU_CACHE); if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) @@ -4574,38 +4621,83 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili colormap = R_GetTranslationColormap(TC_DEFAULT, ref->color, GTC_MENUCACHE); } } - else switch (ref->type) + else { - case SECRET_SKIN: + UINT8 iconid = 0; + switch (ref->type) { - INT32 skin = M_UnlockableSkinNum(ref); - if (skin != -1) + case SECRET_SKIN: { - colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE); - pat = faceprefix[skin][(ref->majorunlock) ? FACE_WANTED : FACE_RANK]; + INT32 skin = M_UnlockableSkinNum(ref); + if (skin != -1) + { + colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE); + pat = faceprefix[skin][(ref->majorunlock) ? FACE_WANTED : FACE_RANK]; + } + break; + } + case SECRET_FOLLOWER: + { + INT32 skin = M_UnlockableFollowerNum(ref); + if (skin != -1) + { + UINT16 col = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value); + colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); + pat = W_CachePatchName(followers[skin].icon, PU_CACHE); + } + break; + } + + /*case SECRET_MASTERBOTS: + iconid = 4; + break;*/ + case SECRET_HARDSPEED: + iconid = 3; + break; + case SECRET_ENCORE: + iconid = 5; + break; + + case SECRET_ALTTITLE: + iconid = 6; + break; + case SECRET_SOUNDTEST: + iconid = 1; + break; + + case SECRET_TIMEATTACK: + iconid = 7; + break; + case SECRET_BREAKTHECAPSULES: + iconid = 8; + break; + case SECRET_SPECIALATTACK: + iconid = 9; + break; + + default: + { + if (!colormap && ref->color != SKINCOLOR_NONE && ref->color < numskincolors) + { + colormap = R_GetTranslationColormap(TC_RAINBOW, ref->color, GTC_MENUCACHE); + } + break; } - break; } - case SECRET_FOLLOWER: + + if (pat == missingpat) { - INT32 skin = M_UnlockableFollowerNum(ref); - if (skin != -1) + pat = W_CachePatchName(va("UN_IC%02u%c", + iconid, + ref->majorunlock ? 'B' : 'A'), + PU_CACHE); + if (pat == missingpat) { - UINT16 col = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value); - colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); - pat = W_CachePatchName(followers[skin].icon, PU_CACHE); + pat = W_CachePatchName(va("UN_IC%02u%c", + iconid, + ref->majorunlock ? 'A' : 'B'), + PU_CACHE); } - break; - } - default: - { - pat = W_CachePatchName(va("UN_RR00%c", ref->majorunlock ? 'B' : 'A'), PU_CACHE); - if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) - { - //CONS_Printf(" color for %d is %s\n", num, skincolors[unlockables[num].color].name); - colormap = R_GetTranslationColormap(TC_RAINBOW, ref->color, GTC_MENUCACHE); - } - break; } } From 9cb176de36186359bdc930c25fddf6fbfcc82c70 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 24 Feb 2023 00:16:39 +0000 Subject: [PATCH 09/38] M_DrawCharSelectExplosions: Fix alignment of explosions for Challenges Grid --- src/k_menudraw.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index d617e87cd..2d9f570b3 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -1479,7 +1479,7 @@ static void M_DrawCharSelectPreview(UINT8 num) static void M_DrawCharSelectExplosions(boolean charsel, INT16 basex, INT16 basey) { UINT8 i; - INT16 quadx = 0, quady = 0; + INT16 quadx = 2, quady = 2, mul = 22; for (i = 0; i < CSEXPLOSIONS; i++) { @@ -1495,13 +1495,14 @@ static void M_DrawCharSelectExplosions(boolean charsel, INT16 basex, INT16 basey { quadx = 4 * (setup_explosions[i].x / 3); quady = 4 * (setup_explosions[i].y / 3); + mul = 16; } colormap = R_GetTranslationColormap(TC_DEFAULT, setup_explosions[i].color, GTC_MENUCACHE); V_DrawMappedPatch( - basex + (setup_explosions[i].x*16) + quadx - 6, - basey + (setup_explosions[i].y*16) + quady - 6, + basex + (setup_explosions[i].x*mul) + quadx - 6, + basey + (setup_explosions[i].y*mul) + quady - 6, 0, W_CachePatchName(va("CHCNFRM%d", frame), PU_CACHE), colormap ); From 917acfd16b411a2318d954398551b422184ce7d9 Mon Sep 17 00:00:00 2001 From: James R Date: Thu, 23 Feb 2023 18:33:35 -0800 Subject: [PATCH 10/38] Refactor pointlimit to be delayed until next round, like timelimit --- src/d_netcmd.c | 35 ++++++++++++++++++++++------------- src/d_netcmd.h | 1 + src/hu_stuff.c | 8 ++++---- src/k_kart.c | 6 ++++++ src/lua_script.c | 2 +- src/p_inter.c | 6 +++--- src/p_saveg.c | 4 ++++ 7 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 96b8cce4c..10a697479 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -4942,23 +4942,30 @@ static void PointLimit_OnChange(void) return; } - // Don't allow pointlimit in non-pointlimited gametypes! - if (server && Playing() && !(gametyperules & GTR_POINTLIMIT)) + if (gamestate == GS_LEVEL && leveltime < starttime) { if (cv_pointlimit.value) - CV_StealthSetValue(&cv_pointlimit, 0); - return; - } + { + CONS_Printf(M_GetText("Point limit has been set to %d.\n"), cv_pointlimit.value); + } + else + { + CONS_Printf(M_GetText("Point limit has been disabled.\n")); + } - if (cv_pointlimit.value) - { - CONS_Printf(M_GetText("Levels will end after %s scores %d point%s.\n"), - G_GametypeHasTeams() ? M_GetText("a team") : M_GetText("someone"), - cv_pointlimit.value, - cv_pointlimit.value > 1 ? "s" : ""); + g_pointlimit = cv_pointlimit.value; } else - CONS_Printf(M_GetText("Point limit disabled\n")); + { + if (cv_pointlimit.value) + { + CONS_Printf(M_GetText("Point limit will be %d next round.\n"), cv_pointlimit.value); + } + else + { + CONS_Printf(M_GetText("Point limit will be disabled next round.\n")); + } + } } static void NetTimeout_OnChange(void) @@ -4983,6 +4990,8 @@ UINT32 timelimitintics = 0; UINT32 extratimeintics = 0; UINT32 secretextratime = 0; +UINT32 g_pointlimit = 0; + /** Deals with a timelimit change by printing the change to the console. * If the gametype is single player, cooperative, or race, the timelimit is * silently disabled again. @@ -6463,7 +6472,7 @@ static void Command_ShowScores_f(void) // FIXME: %lu? what's wrong with %u? ~Callum (produces warnings...) CONS_Printf(M_GetText("%s's score is %u\n"), player_names[i], players[i].score); } - CONS_Printf(M_GetText("The pointlimit is %d\n"), cv_pointlimit.value); + CONS_Printf(M_GetText("The pointlimit is %d\n"), g_pointlimit); } diff --git a/src/d_netcmd.h b/src/d_netcmd.h index c9006a7ba..61171d393 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -61,6 +61,7 @@ extern consvar_t cv_pointlimit; extern consvar_t cv_timelimit; extern consvar_t cv_numlaps; extern UINT32 timelimitintics, extratimeintics, secretextratime; +extern UINT32 g_pointlimit; extern consvar_t cv_allowexitlevel; extern consvar_t cv_autobalance; diff --git a/src/hu_stuff.c b/src/hu_stuff.c index a457fe774..cd4277dc6 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -2453,10 +2453,10 @@ static void HU_DrawRankings(void) timedone = true; } - else if ((gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0) + else if ((gametyperules & GTR_POINTLIMIT) && g_pointlimit > 0) { V_DrawCenteredString(64, 8, 0, "POINT LIMIT"); - V_DrawCenteredString(64, 16, hilicol, va("%d", cv_pointlimit.value)); + V_DrawCenteredString(64, 16, hilicol, va("%d", g_pointlimit)); pointsdone = true; } else if (gametyperules & GTR_CIRCUIT) @@ -2494,10 +2494,10 @@ static void HU_DrawRankings(void) V_DrawCenteredString(256, 16, hilicol, "OVERTIME"); } } - else if (!pointsdone && (gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0) + else if (!pointsdone && (gametyperules & GTR_POINTLIMIT) && g_pointlimit > 0) { V_DrawCenteredString(256, 8, 0, "POINT LIMIT"); - V_DrawCenteredString(256, 16, hilicol, va("%d", cv_pointlimit.value)); + V_DrawCenteredString(256, 16, hilicol, va("%d", g_pointlimit)); } else if (gametyperules & GTR_CIRCUIT) { diff --git a/src/k_kart.c b/src/k_kart.c index 45a8eb900..ce485d07c 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -100,6 +100,7 @@ void K_TimerReset(void) numbulbs = 1; inDuel = rainbowstartavailable = false; timelimitintics = extratimeintics = secretextratime = 0; + g_pointlimit = 0; } void K_TimerInit(void) @@ -206,6 +207,11 @@ void K_TimerInit(void) } } + if (gametyperules & GTR_POINTLIMIT) + { + g_pointlimit = cv_pointlimit.value; + } + if (inDuel == true) { K_SpawnDuelOnlyItems(); diff --git a/src/lua_script.c b/src/lua_script.c index 17dd61a8d..ed35d9e69 100644 --- a/src/lua_script.c +++ b/src/lua_script.c @@ -205,7 +205,7 @@ int LUA_PushGlobals(lua_State *L, const char *word) lua_pushinteger(L, timelimitintics); return 1; } else if (fastcmp(word,"pointlimit")) { - lua_pushinteger(L, cv_pointlimit.value); + lua_pushinteger(L, g_pointlimit); return 1; // begin map vars } else if (fastcmp(word,"titlemap")) { diff --git a/src/p_inter.c b/src/p_inter.c index 8a13e5f8c..84f88b022 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -797,7 +797,7 @@ void P_CheckPointLimit(void) if (!K_CanChangeRules(true)) return; - if (!cv_pointlimit.value) + if (!g_pointlimit) return; if (!(gametyperules & GTR_POINTLIMIT)) @@ -810,7 +810,7 @@ void P_CheckPointLimit(void) if (G_GametypeHasTeams()) { // Just check both teams - if ((UINT32)cv_pointlimit.value <= redscore || (UINT32)cv_pointlimit.value <= bluescore) + if (g_pointlimit <= redscore || g_pointlimit <= bluescore) { if (server) SendNetXCmd(XD_EXITLEVEL, NULL, 0); @@ -823,7 +823,7 @@ void P_CheckPointLimit(void) if (!playeringame[i] || players[i].spectator) continue; - if ((UINT32)cv_pointlimit.value <= players[i].roundscore) + if (g_pointlimit <= players[i].roundscore) { for (i = 0; i < MAXPLAYERS; i++) // AAAAA nested loop using the same iteration variable ;; { diff --git a/src/p_saveg.c b/src/p_saveg.c index 453eae49c..6c27d8d91 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -4984,6 +4984,8 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending) WRITEUINT32(save->p, extratimeintics); WRITEUINT32(save->p, secretextratime); + WRITEUINT32(save->p, g_pointlimit); + // Is it paused? if (paused) WRITEUINT8(save->p, 0x2f); @@ -5154,6 +5156,8 @@ static inline boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading) extratimeintics = READUINT32(save->p); secretextratime = READUINT32(save->p); + g_pointlimit = READUINT32(save->p); + // Is it paused? if (READUINT8(save->p) == 0x2f) paused = true; From 9b5bd70caeb3c78157baf3cec3c374f47a493e46 Mon Sep 17 00:00:00 2001 From: AJ Martinez Date: Fri, 24 Feb 2023 00:30:21 -0700 Subject: [PATCH 11/38] Briefly lock out TRIPWIRE_BLASTER after failing a tripwire --- src/d_player.h | 2 ++ src/k_kart.c | 12 +++++++++++- src/lua_playerlib.c | 4 ++++ src/p_saveg.c | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/d_player.h b/src/d_player.h index e71063fe4..12cab4a9a 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -661,6 +661,8 @@ struct player_t UINT8 eggmanTransferDelay; + UINT8 tripwireReboundDelay; // When failing Tripwire, brieftly lock out speed-based tripwire pass (anti-cheese) + mobj_t *stumbleIndicator; #ifdef HWRENDER diff --git a/src/k_kart.c b/src/k_kart.c index 45a8eb900..01778ab86 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -2683,7 +2683,7 @@ tripwirepass_t K_TripwirePassConditions(player_t *player) if ( player->flamedash || - player->speed > 2 * K_GetKartSpeed(player, false, false) + (player->speed > 2 * K_GetKartSpeed(player, false, false) && player->tripwireReboundDelay == 0) ) return TRIPWIRE_BOOST; @@ -4078,10 +4078,17 @@ void K_TumbleInterrupt(player_t *player) void K_ApplyTripWire(player_t *player, tripwirestate_t state) { + // We are either softlocked or wildly misbehaving. Stop that! + if (state == TRIPSTATE_BLOCKED && player->tripwireReboundDelay && (player->speed > 5 * K_GetKartSpeed(player, false, false))) + K_TumblePlayer(player, NULL, NULL); + if (state == TRIPSTATE_PASSED) S_StartSound(player->mo, sfx_ssa015); else if (state == TRIPSTATE_BLOCKED) + { S_StartSound(player->mo, sfx_kc40); + player->tripwireReboundDelay = 60; + } player->tripwireState = state; K_AddHitLag(player->mo, 10, false); @@ -7713,6 +7720,9 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) if (player->eggmanTransferDelay) player->eggmanTransferDelay--; + if (player->tripwireReboundDelay) + player->tripwireReboundDelay--; + if (player->ringdelay) player->ringdelay--; diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c index 7195c90f0..211f83867 100644 --- a/src/lua_playerlib.c +++ b/src/lua_playerlib.c @@ -306,6 +306,8 @@ static int player_get(lua_State *L) lua_pushinteger(L, plr->tripwirePass); else if (fastcmp(field,"tripwireLeniency")) lua_pushinteger(L, plr->tripwireLeniency); + else if (fastcmp(field,"tripwireReboundDelay")) + lua_pushinteger(L, plr->tripwireReboundDelay); /* else if (fastcmp(field,"itemroulette")) lua_pushinteger(L, plr->itemroulette); @@ -684,6 +686,8 @@ static int player_set(lua_State *L) plr->tripwirePass = luaL_checkinteger(L, 3); else if (fastcmp(field,"tripwireLeniency")) plr->tripwireLeniency = luaL_checkinteger(L, 3); + else if (fastcmp(field,"tripwireReboundDelay")) + plr->tripwireReboundDelay = luaL_checkinteger(L, 3); /* else if (fastcmp(field,"itemroulette")) plr->itemroulette = luaL_checkinteger(L, 3); diff --git a/src/p_saveg.c b/src/p_saveg.c index 453eae49c..75c08b834 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -395,6 +395,8 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT8(save->p, players[i].eggmanTransferDelay); + WRITEUINT8(save->p, players[i].tripwireReboundDelay); + // respawnvars_t WRITEUINT8(save->p, players[i].respawn.state); WRITEUINT32(save->p, K_GetWaypointHeapIndex(players[i].respawn.wp)); @@ -750,6 +752,8 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].eggmanTransferDelay = READUINT8(save->p); + players[i].tripwireReboundDelay = READUINT8(save->p); + // respawnvars_t players[i].respawn.state = READUINT8(save->p); players[i].respawn.wp = (waypoint_t *)(size_t)READUINT32(save->p); From 4380876959f881f4d9722a745b94d95d957005ab Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 24 Feb 2023 17:38:08 +0000 Subject: [PATCH 12/38] Challenges Grid: create dedicated struct for menu extradata - challengegridextradata_t - Existing UINT8 data per entry has been converted to "flags" property - This is a tool that will help us later. --- src/k_menu.h | 2 +- src/k_menudraw.c | 6 ++--- src/m_cond.c | 43 +++++++++++++++++++++-------------- src/m_cond.h | 10 +++++++- src/menus/extras-challenges.c | 22 +++++++++--------- src/typedef.h | 1 + 6 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/k_menu.h b/src/k_menu.h index ec6a96111..33b08da0a 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1151,7 +1151,7 @@ extern struct challengesmenu_s { SINT8 row, hilix, focusx; UINT8 col, hiliy; - UINT8 *extradata; + challengegridextradata_t *extradata; boolean pending; boolean requestnew; diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 2d9f570b3..56709a7ed 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4544,7 +4544,7 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili if (!unlockedyet) { - UINT16 col = (challengesmenu.extradata[id] == CHE_HINT) ? SKINCOLOR_BLUE : SKINCOLOR_BLACK; + UINT16 col = (challengesmenu.extradata[id].flags == CHE_HINT) ? SKINCOLOR_BLUE : SKINCOLOR_BLACK; bgmap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); } else @@ -5027,7 +5027,7 @@ void M_DrawChallenges(void) { y += challengesgridstep; - if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j] & CHE_DONTDRAW) + if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j].flags & CHE_DONTDRAW) { continue; } @@ -5112,7 +5112,7 @@ challengedesc: && challengesmenu.currentunlock < MAXUNLOCKABLES && ((gamedata->unlocked[challengesmenu.currentunlock] == true) || ((challengesmenu.extradata != NULL) - && (challengesmenu.extradata[i] & CHE_HINT)) + && (challengesmenu.extradata[i].flags & CHE_HINT)) ) ) { diff --git a/src/m_cond.c b/src/m_cond.c index ffcbca21b..26d91d6a0 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -260,10 +260,10 @@ quickcheckagain: } } -UINT8 *M_ChallengeGridExtraData(void) +challengegridextradata_t *M_ChallengeGridExtraData(void) { UINT8 i, j, num, id, tempid, work; - UINT8 *extradata; + challengegridextradata_t *extradata; boolean idchange; if (!gamedata->challengegrid) @@ -272,7 +272,7 @@ UINT8 *M_ChallengeGridExtraData(void) } extradata = Z_Malloc( - (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)), + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(challengegridextradata_t)), PU_STATIC, NULL); if (!extradata) @@ -282,6 +282,17 @@ UINT8 *M_ChallengeGridExtraData(void) //CONS_Printf(" --- \n"); + // Pre-wipe flags. + for (i = 0; i < gamedata->challengegridwidth; i++) + { + for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) + { + id = (i * CHALLENGEGRIDHEIGHT) + j; + extradata[id].flags = CHE_NONE; + } + } + + // Populate extra data. for (i = 0; i < gamedata->challengegridwidth; i++) { for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) @@ -290,8 +301,6 @@ UINT8 *M_ChallengeGridExtraData(void) num = gamedata->challengegrid[id]; idchange = false; - extradata[id] = CHE_NONE; - // Empty spots in the grid are always unconnected. if (num >= MAXUNLOCKABLES) { @@ -305,13 +314,13 @@ UINT8 *M_ChallengeGridExtraData(void) work = gamedata->challengegrid[tempid]; if (work == num) { - extradata[id] = CHE_CONNECTEDUP; + extradata[id].flags = CHE_CONNECTEDUP; // Get the id to write extra hint data to. // This check is safe because extradata's order of population - if (extradata[tempid] & CHE_CONNECTEDLEFT) + if (extradata[tempid].flags & CHE_CONNECTEDLEFT) { - extradata[id] |= CHE_CONNECTEDLEFT; + extradata[id].flags |= CHE_CONNECTEDLEFT; //CONS_Printf(" %d - %d above %d is invalid, check to left\n", num, tempid, id); if (i > 0) { @@ -328,14 +337,14 @@ UINT8 *M_ChallengeGridExtraData(void) id = tempid; idchange = true; - if (extradata[id] == CHE_HINT) + if (extradata[id].flags == CHE_HINT) { continue; } } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; } } @@ -357,11 +366,11 @@ UINT8 *M_ChallengeGridExtraData(void) { //CONS_Printf(" %d - %d to left of %d is valid\n", work, tempid, id); // If we haven't already updated our id, it's the one to our left. - if (extradata[id] == CHE_HINT) + if (extradata[id].flags == CHE_HINT) { - extradata[tempid] = CHE_HINT; + extradata[tempid].flags = CHE_HINT; } - extradata[id] = CHE_CONNECTEDLEFT; + extradata[id].flags = CHE_CONNECTEDLEFT; id = tempid; } /*else @@ -369,13 +378,13 @@ UINT8 *M_ChallengeGridExtraData(void) } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; continue; } } // Since we're not modifying id past this point, the conditions become much simpler. - if (extradata[id] == CHE_HINT) + if ((extradata[id].flags & (CHE_HINT|CHE_DONTDRAW)) == CHE_HINT) { continue; } @@ -392,7 +401,7 @@ UINT8 *M_ChallengeGridExtraData(void) } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; continue; } } @@ -415,7 +424,7 @@ UINT8 *M_ChallengeGridExtraData(void) } else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) { - extradata[id] = CHE_HINT; + extradata[id].flags = CHE_HINT; continue; } } diff --git a/src/m_cond.h b/src/m_cond.h index 9439cfe86..4d8ac1c2b 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -193,12 +193,20 @@ void M_NewGameDataStruct(void); // Challenges menu stuff void M_PopulateChallengeGrid(void); -UINT8 *M_ChallengeGridExtraData(void); + +struct challengegridextradata_t +{ + UINT8 flags; +}; + +challengegridextradata_t *M_ChallengeGridExtraData(void); + #define CHE_NONE 0 #define CHE_HINT 1 #define CHE_CONNECTEDLEFT (1<<1) #define CHE_CONNECTEDUP (1<<2) #define CHE_DONTDRAW (CHE_CONNECTEDLEFT|CHE_CONNECTEDUP) + char *M_BuildConditionSetString(UINT8 unlockid); #define DESCRIPTIONWIDTH 170 diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 583dc7a1a..e38415bdf 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -72,7 +72,7 @@ static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh) continue; } - if (challengesmenu.extradata[i] & CHE_CONNECTEDLEFT) + if (challengesmenu.extradata[i].flags & CHE_CONNECTEDLEFT) { // no need to check for CHE_CONNECTEDUP in linear iteration continue; @@ -461,8 +461,8 @@ boolean M_ChallengesInputs(INT32 ch) } if (!(challengesmenu.extradata[ (challengesmenu.col * CHALLENGEGRIDHEIGHT) - + challengesmenu.row] - & CHE_CONNECTEDUP)) + + challengesmenu.row + ].flags & CHE_CONNECTEDUP)) { break; } @@ -475,8 +475,8 @@ boolean M_ChallengesInputs(INT32 ch) { i = (challengesmenu.extradata[ (challengesmenu.col * CHALLENGEGRIDHEIGHT) - + challengesmenu.row] - & CHE_CONNECTEDUP) ? 2 : 1; + + challengesmenu.row + ].flags & CHE_CONNECTEDUP) ? 2 : 1; while (i > 0) { if (challengesmenu.row > 0) @@ -516,8 +516,8 @@ boolean M_ChallengesInputs(INT32 ch) if (!(challengesmenu.extradata[ (challengesmenu.col * CHALLENGEGRIDHEIGHT) - + challengesmenu.row] - & CHE_CONNECTEDLEFT)) + + challengesmenu.row + ].flags & CHE_CONNECTEDLEFT)) { break; } @@ -531,8 +531,8 @@ boolean M_ChallengesInputs(INT32 ch) { i = (challengesmenu.extradata[ (challengesmenu.col * CHALLENGEGRIDHEIGHT) - + challengesmenu.row] - & CHE_CONNECTEDLEFT) ? 2 : 1; + + challengesmenu.row + ].flags & CHE_CONNECTEDLEFT) ? 2 : 1; while (i > 0) { // Slide the focus counter to movement, if we can. @@ -570,12 +570,12 @@ boolean M_ChallengesInputs(INT32 ch) { // Adjust highlight coordinates up/to the left for large tiles. - if (challengesmenu.hiliy > 0 && (challengesmenu.extradata[i] & CHE_CONNECTEDUP)) + if (challengesmenu.hiliy > 0 && (challengesmenu.extradata[i].flags & CHE_CONNECTEDUP)) { challengesmenu.hiliy--; } - if ((challengesmenu.extradata[i] & CHE_CONNECTEDLEFT)) + if ((challengesmenu.extradata[i].flags & CHE_CONNECTEDLEFT)) { if (challengesmenu.hilix > 0) { diff --git a/src/typedef.h b/src/typedef.h index a38b018a0..632684d1e 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -209,6 +209,7 @@ TYPEDEF (conditionset_t); TYPEDEF (emblem_t); TYPEDEF (unlockable_t); TYPEDEF (gamedata_t); +TYPEDEF (challengegridextradata_t); // m_dllist.h TYPEDEF (mdllistitem_t); From e65bb87ec557a820165be7a2586d4054b08722d8 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 24 Feb 2023 18:15:38 +0000 Subject: [PATCH 13/38] M_DrawChallengeTile: Change the appearance of locked tiles - Question mark - If next to an unlocked tile (ie, hint mode) - Have ring around when highlighted, per hint ring - ELSE, make blink-colormapped + subtractive --- src/k_menudraw.c | 49 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 56709a7ed..7ebc481a5 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4538,19 +4538,44 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili unlockedyet = !((gamedata->unlocked[num] == false) || (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME)); + { + UINT32 flags = 0; + + if (challengesmenu.extradata[id].flags != CHE_HINT) + { + colormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_CACHE); + flags = V_SUBTRACT|V_90TRANS; + } + + pat = W_CachePatchName( + va("UN_HNT%c%c", + (hili && !colormap) ? '1' : '2', + ref->majorunlock ? 'B' : 'A' + ), + PU_CACHE); + + V_DrawFixedPatch( + x*FRACUNIT, y*FRACUNIT, + FRACUNIT, + flags, pat, + colormap + ); + + pat = missingpat; + colormap = NULL; + } + + // If we aren't unlocked yet, return early. + if (!unlockedyet) + { + goto drawborder; + } + pat = W_CachePatchName( (ref->majorunlock ? "UN_BORDB" : "UN_BORDA"), PU_CACHE); - if (!unlockedyet) - { - UINT16 col = (challengesmenu.extradata[id].flags == CHE_HINT) ? SKINCOLOR_BLUE : SKINCOLOR_BLACK; - bgmap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE); - } - else - { - bgmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE); - } + bgmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE); V_DrawFixedPatch( x*FRACUNIT, y*FRACUNIT, @@ -4561,12 +4586,6 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili pat = missingpat; - // If we aren't unlocked yet, return early. - if (!unlockedyet) - { - goto drawborder; - } - categoryside = !hili; // temporary if (categoryside) From 8dc4971c2a6303c148cf7a0bb5888dd161da79da Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 24 Feb 2023 19:06:45 +0000 Subject: [PATCH 14/38] M_DrawChallengeTile: Don't draw the ? stuff if not yet unlocked --- src/k_menudraw.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index 7ebc481a5..fc44aadde 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4538,6 +4538,8 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili unlockedyet = !((gamedata->unlocked[num] == false) || (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME)); + // If we aren't unlocked yet, return early. + if (!unlockedyet) { UINT32 flags = 0; @@ -4563,11 +4565,7 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili pat = missingpat; colormap = NULL; - } - // If we aren't unlocked yet, return early. - if (!unlockedyet) - { goto drawborder; } From 339617a54e6fce9dc5f099c4b8db07c0b6651f49 Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 24 Feb 2023 19:10:44 +0000 Subject: [PATCH 15/38] extras-challenges.c, M_UpdateChallengeGridExtraData: Be less lazy about reallocation - Z_Calloc once, on menu load, instead of every time data changes - Free once, on menu exit, instead of every time data changes --- src/m_cond.c | 17 +++++------------ src/m_cond.h | 2 +- src/menus/extras-challenges.c | 22 +++++++++++++++------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/m_cond.c b/src/m_cond.c index 26d91d6a0..f948f400d 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -260,24 +260,19 @@ quickcheckagain: } } -challengegridextradata_t *M_ChallengeGridExtraData(void) +void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { UINT8 i, j, num, id, tempid, work; - challengegridextradata_t *extradata; boolean idchange; - if (!gamedata->challengegrid) + if (gamedata->challengegrid == NULL) { - return NULL; + return; } - extradata = Z_Malloc( - (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(challengegridextradata_t)), - PU_STATIC, NULL); - - if (!extradata) + if (extradata == NULL) { - I_Error("M_ChallengeGridExtraData: was not able to allocate extradata"); + return; } //CONS_Printf(" --- \n"); @@ -430,8 +425,6 @@ challengegridextradata_t *M_ChallengeGridExtraData(void) } } } - - return extradata; } void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2) diff --git a/src/m_cond.h b/src/m_cond.h index 4d8ac1c2b..f8fa531d2 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -199,7 +199,7 @@ struct challengegridextradata_t UINT8 flags; }; -challengegridextradata_t *M_ChallengeGridExtraData(void); +void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata); #define CHE_NONE 0 #define CHE_HINT 1 diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index e38415bdf..fb8598eff 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -174,7 +174,12 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) M_PopulateChallengeGrid(); if (gamedata->challengegrid) - challengesmenu.extradata = M_ChallengeGridExtraData(); + { + challengesmenu.extradata = Z_Calloc( + (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(challengegridextradata_t)), + PU_STATIC, NULL); + M_UpdateChallengeGridExtraData(challengesmenu.extradata); + } memset(setup_explosions, 0, sizeof(setup_explosions)); memset(&challengesmenu.unlockcount, 0, sizeof(challengesmenu.unlockcount)); @@ -318,11 +323,15 @@ void M_ChallengesTick(void) challengesmenu.unlockcount[CC_TALLY]++; challengesmenu.unlockcount[CC_ANIM]++; - Z_Free(challengesmenu.extradata); - if ((challengesmenu.extradata = M_ChallengeGridExtraData())) + if (challengesmenu.extradata) { - unlockable_t *ref = &unlockables[challengesmenu.currentunlock]; - UINT16 bombcolor = SKINCOLOR_NONE; + unlockable_t *ref; + UINT16 bombcolor; + + M_UpdateChallengeGridExtraData(challengesmenu.extradata); + + ref = &unlockables[challengesmenu.currentunlock]; + bombcolor = SKINCOLOR_NONE; if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors) { @@ -413,8 +422,7 @@ boolean M_ChallengesInputs(INT32 ch) gamedata->challengegrid = NULL; gamedata->challengegridwidth = 0; M_PopulateChallengeGrid(); - Z_Free(challengesmenu.extradata); - challengesmenu.extradata = M_ChallengeGridExtraData(); + M_UpdateChallengeGridExtraData(challengesmenu.extradata); M_ChallengesAutoFocus(challengesmenu.currentunlock, true); From 9779d6066ca576222751492e157893a7241ea500 Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 25 Feb 2023 00:21:28 +0000 Subject: [PATCH 16/38] Challenge Grid: Introduce flipping tiles - When a tile is selected, flips from category side to specific icon side - Flips back when unselected - Hold R (drift) to flip all tiles to visible side --- src/k_menu.h | 2 ++ src/k_menudraw.c | 31 +++++++++++++++++++------- src/m_cond.h | 1 + src/menus/extras-challenges.c | 42 ++++++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/k_menu.h b/src/k_menu.h index 33b08da0a..fb08ff4ab 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1137,6 +1137,8 @@ void M_DrawAddons(void); #define CC_ANIM 3 #define CC_MAX 4 +#define TILEFLIP_MAX 16 + // Keep track of some pause menu data for visual goodness. extern struct challengesmenu_s { diff --git a/src/k_menudraw.c b/src/k_menudraw.c index fc44aadde..5f5f69726 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -4518,7 +4518,7 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili unlockable_t *ref = NULL; patch_t *pat = missingpat; UINT8 *colormap = NULL, *bgmap = NULL; - fixed_t siz; + fixed_t siz, accordion; UINT8 id, num; boolean unlockedyet; boolean categoryside; @@ -4569,14 +4569,26 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili goto drawborder; } + accordion = FRACUNIT; + + if (challengesmenu.extradata[id].flip != 0 + && challengesmenu.extradata[id].flip != (TILEFLIP_MAX/2)) + { + angle_t bad = (FixedAngle((fixed_t)(challengesmenu.extradata[id].flip) * (360*FRACUNIT/TILEFLIP_MAX)) >> ANGLETOFINESHIFT) & FINEMASK; + accordion = FINECOSINE(bad); + if (accordion < 0) + accordion = -accordion; + } + pat = W_CachePatchName( (ref->majorunlock ? "UN_BORDB" : "UN_BORDA"), PU_CACHE); bgmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE); - V_DrawFixedPatch( - x*FRACUNIT, y*FRACUNIT, + V_DrawStretchyFixedPatch( + (x*FRACUNIT) + (SHORT(pat->width)*(FRACUNIT-accordion)/2), y*FRACUNIT, + accordion, FRACUNIT, 0, pat, bgmap @@ -4584,7 +4596,8 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili pat = missingpat; - categoryside = !hili; // temporary + categoryside = (challengesmenu.extradata[id].flip <= TILEFLIP_MAX/4 + || challengesmenu.extradata[id].flip > (3*TILEFLIP_MAX)/4); if (categoryside) { @@ -4725,8 +4738,9 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili ; // prevent div/0 else if (ref->majorunlock) { - V_DrawFixedPatch( - (x + 5)*FRACUNIT, (y + 5)*FRACUNIT, + V_DrawStretchyFixedPatch( + ((x + 5)*FRACUNIT) + (32*(FRACUNIT-accordion)/2), (y + 5)*FRACUNIT, + FixedDiv(32*accordion, siz), FixedDiv(32 << FRACBITS, siz), 0, pat, colormap @@ -4734,8 +4748,9 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili } else { - V_DrawFixedPatch( - (x + 2)*FRACUNIT, (y + 2)*FRACUNIT, + V_DrawStretchyFixedPatch( + ((x + 2)*FRACUNIT) + (16*(FRACUNIT-accordion)/2), (y + 2)*FRACUNIT, + FixedDiv(16*accordion, siz), FixedDiv(16 << FRACBITS, siz), 0, pat, colormap diff --git a/src/m_cond.h b/src/m_cond.h index f8fa531d2..367f3d720 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -197,6 +197,7 @@ void M_PopulateChallengeGrid(void); struct challengegridextradata_t { UINT8 flags; + UINT8 flip; }; void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata); diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index fb8598eff..a9d056cd4 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -84,6 +84,16 @@ static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh) challengesmenu.col = challengesmenu.hilix = i/CHALLENGEGRIDHEIGHT; challengesmenu.row = challengesmenu.hiliy = i%CHALLENGEGRIDHEIGHT; + // Begin animation + if (challengesmenu.extradata[i].flip == 0) + { + challengesmenu.extradata[i].flip = + (challengesmenu.pending + ? (TILEFLIP_MAX/2) + : 1 + ); + } + if (fresh) { // We're just entering the menu. Immediately jump to the desired position... @@ -261,7 +271,8 @@ void M_Challenges(INT32 choice) void M_ChallengesTick(void) { const UINT8 pid = 0; - UINT8 i, newunlock = MAXUNLOCKABLES; + UINT16 i; + UINT8 newunlock = MAXUNLOCKABLES; // Ticking challengesmenu.ticker++; @@ -275,6 +286,29 @@ void M_ChallengesTick(void) challengesmenu.unlockcount[CC_ANIM]--; M_CupSelectTick(); + // Update tile flip state. + if (challengesmenu.extradata != NULL) + { + UINT16 id = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy; + boolean seeeveryone = M_MenuButtonHeld(pid, MBT_R); + boolean allthewaythrough; + UINT8 maxflip; + for (i = 0; i < (CHALLENGEGRIDHEIGHT * gamedata->challengegridwidth); i++) + { + allthewaythrough = (!seeeveryone && !challengesmenu.pending && i != id); + maxflip = ((seeeveryone || !allthewaythrough) ? (TILEFLIP_MAX/2) : TILEFLIP_MAX); + if ((seeeveryone || (challengesmenu.extradata[i].flip > 0)) + && (challengesmenu.extradata[i].flip != maxflip)) + { + challengesmenu.extradata[i].flip++; + if (challengesmenu.extradata[i].flip >= TILEFLIP_MAX) + { + challengesmenu.extradata[i].flip = 0; + } + } + } + } + if (challengesmenu.pending) { // Pending mode. @@ -594,8 +628,14 @@ boolean M_ChallengesInputs(INT32 ch) challengesmenu.hilix = gamedata->challengegridwidth-1; } } + + i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy; } + // Begin animation + if (challengesmenu.extradata[i].flip == 0) + challengesmenu.extradata[i].flip++; + return true; } From 182768aa32cbd7d7709aee815b74e43d6f6f93eb Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 25 Feb 2023 00:33:56 +0000 Subject: [PATCH 17/38] Unlockable sounds updated - Use sfx_achiev when any Challenge is completed - If picking up MT_EMBLEM *doesn't* immediately complete a challenge, use sfx_ncitem as before - Bugfix: this sound is played even when online, which it wasn't previously doing (because the object couldn't be destroyed and play its sound) --- src/info.c | 2 +- src/m_cond.c | 2 +- src/p_inter.c | 3 ++- src/sounds.c | 1 + src/sounds.h | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/info.c b/src/info.c index 2c22b4bf4..4807869e6 100644 --- a/src/info.c +++ b/src/info.c @@ -8037,7 +8037,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL, // missilestate S_SPRK1, // deathstate S_NULL, // xdeathstate - sfx_ncitem, // deathsound + sfx_None, // deathsound 1, // speed 16*FRACUNIT, // radius 30*FRACUNIT, // height diff --git a/src/m_cond.c b/src/m_cond.c index f948f400d..070c5b9ea 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -912,7 +912,7 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud) { if (loud) { - S_StartSound(NULL, sfx_ncitem); + S_StartSound(NULL, sfx_achiev); } return true; } diff --git a/src/p_inter.c b/src/p_inter.c index 64f96100a..25ddaa59b 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -552,7 +552,8 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) if (P_IsLocalPlayer(player) && !gamedata->collected[special->health-1]) { gamedata->collected[special->health-1] = gotcollected = true; - M_UpdateUnlockablesAndExtraEmblems(true); + if (!M_UpdateUnlockablesAndExtraEmblems(true)) + S_StartSound(NULL, sfx_ncitem); G_SaveGameData(); } diff --git a/src/sounds.c b/src/sounds.c index 207d2713c..0fc59206a 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -1101,6 +1101,7 @@ sfxinfo_t S_sfx[NUMSFX] = {"typri1", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SA2 boss typewriting 1 {"typri2", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SA2 final boss-type typewriting {"eggspr", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // Sonic Unleashed Trap Spring + {"achiev", false, 204, 0, -1, NULL, 0, -1, -1, LUMPERROR, "Achievement"}, // SRB2Kart - Drop target sounds {"kdtrg1", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // Low energy, SF_X8AWAYSOUND diff --git a/src/sounds.h b/src/sounds.h index b6f910c6f..46f1eedf8 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -1168,6 +1168,7 @@ typedef enum sfx_typri1, sfx_typri2, sfx_eggspr, + sfx_achiev, // SRB2Kart - Drop target sounds sfx_kdtrg1, From 3741ab1c9185fb7163afbedf5ec2d3ad9c5acfd4 Mon Sep 17 00:00:00 2001 From: James R Date: Sat, 28 Jan 2023 02:16:04 -0800 Subject: [PATCH 18/38] Add libwebm, libvpx, libyuv dependencies Libvpx is built with configure script and make. Unfortunate but the configure script is very dense and I'm not sure if it's worth translating into CMake, since it apparently does CPU detection for optimizations. --- CMakeLists.txt | 1 + cmake/Modules/FindVPX.cmake | 33 ++++++++++++++++ src/CMakeLists.txt | 2 + thirdparty/CMakeLists.txt | 3 ++ thirdparty/cpm-libvpx.cmake | 37 +++++++++++++++++ thirdparty/cpm-libwebm.cmake | 31 +++++++++++++++ thirdparty/cpm-libyuv.cmake | 77 ++++++++++++++++++++++++++++++++++++ 7 files changed, 184 insertions(+) create mode 100644 cmake/Modules/FindVPX.cmake create mode 100644 thirdparty/cpm-libvpx.cmake create mode 100644 thirdparty/cpm-libwebm.cmake create mode 100644 thirdparty/cpm-libyuv.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 151a7a389..de4b96ceb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,7 @@ if("${SRB2_CONFIG_SYSTEM_LIBRARIES}") find_package(SDL2 REQUIRED) find_package(CURL REQUIRED) find_package(GME REQUIRED) + find_package(VPX REQUIRED) endif() if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR}) diff --git a/cmake/Modules/FindVPX.cmake b/cmake/Modules/FindVPX.cmake new file mode 100644 index 000000000..f47aee65f --- /dev/null +++ b/cmake/Modules/FindVPX.cmake @@ -0,0 +1,33 @@ +include(LibFindMacros) + +libfind_pkg_check_modules(VPX_PKGCONF VPX) + +find_path(VPX_INCLUDE_DIR + NAMES vpx/vp8.h + PATHS + ${VPX_PKGCONF_INCLUDE_DIRS} + "/usr/include" + "/usr/local/include" +) + +find_library(VPX_LIBRARY + NAMES vpx + PATHS + ${VPX_PKGCONF_LIBRARY_DIRS} + "/usr/lib" + "/usr/local/lib" +) + +set(VPX_PROCESS_INCLUDES VPX_INCLUDE_DIR) +set(VPX_PROCESS_LIBS VPX_LIBRARY) +libfind_process(VPX) + +if(VPX_FOUND AND NOT TARGET webm::libvpx) + add_library(webm::libvpx UNKNOWN IMPORTED) + set_target_properties( + webm::libvpx + PROPERTIES + IMPORTED_LOCATION "${VPX_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${VPX_INCLUDE_DIR}" + ) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ac592f89d..7cd3db33d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -229,6 +229,8 @@ target_link_libraries(SRB2SDL2 PRIVATE xmp-lite::xmp-lite) target_link_libraries(SRB2SDL2 PRIVATE glad::glad) target_link_libraries(SRB2SDL2 PRIVATE fmt) target_link_libraries(SRB2SDL2 PRIVATE imgui::imgui) +target_link_libraries(SRB2SDL2 PRIVATE webm::libwebm webm::libvpx) +target_link_libraries(SRB2SDL2 PRIVATE libyuv::libyuv) target_link_libraries(SRB2SDL2 PRIVATE acsvm) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 479fd26a9..d4cad6bba 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -15,6 +15,7 @@ include("cpm-sdl2.cmake") include("cpm-png.cmake") include("cpm-curl.cmake") include("cpm-libgme.cmake") + include("cpm-libvpx.cmake") endif() include("cpm-rapidjson.cmake") @@ -23,6 +24,8 @@ include("cpm-xmp-lite.cmake") include("cpm-fmt.cmake") include("cpm-imgui.cmake") include("cpm-acsvm.cmake") +include("cpm-libwebm.cmake") +include("cpm-libyuv.cmake") add_subdirectory(tcbrindle_span) add_subdirectory(stb_vorbis) diff --git a/thirdparty/cpm-libvpx.cmake b/thirdparty/cpm-libvpx.cmake new file mode 100644 index 000000000..022f133ca --- /dev/null +++ b/thirdparty/cpm-libvpx.cmake @@ -0,0 +1,37 @@ +CPMAddPackage( + NAME libvpx + VERSION 1.12.0 + URL "https://chromium.googlesource.com/webm/libvpx/+archive/03265cd42b3783532de72f2ded5436652e6f5ce3.tar.gz" + EXCLUDE_FROM_ALL ON + DOWNLOAD_ONLY YES +) + +if(libvpx_ADDED) + include(ExternalProject) + + # libvpx configure script does CPU detection. So lets just + # call it instead of trying to do all that in CMake. + ExternalProject_Add(libvpx + PREFIX "${libvpx_BINARY_DIR}" + SOURCE_DIR "${libvpx_SOURCE_DIR}" + BINARY_DIR "${libvpx_BINARY_DIR}" + CONFIGURE_COMMAND sh "${libvpx_SOURCE_DIR}/configure" + --enable-vp8 --disable-vp9 --disable-vp8-decoder + --disable-examples --disable-tools --disable-docs + --disable-webm-io --disable-libyuv --disable-unit-tests + BUILD_COMMAND "make" + BUILD_BYPRODUCTS "${libvpx_BINARY_DIR}/libvpx.a" + INSTALL_COMMAND "" + USES_TERMINAL_CONFIGURE ON + USES_TERMINAL_BUILD ON + ) + + add_library(webm::libvpx STATIC IMPORTED GLOBAL) + add_dependencies(webm::libvpx libvpx) + set_target_properties( + webm::libvpx + PROPERTIES + IMPORTED_LOCATION "${libvpx_BINARY_DIR}/libvpx.a" + INTERFACE_INCLUDE_DIRECTORIES "${libvpx_SOURCE_DIR}" + ) +endif() diff --git a/thirdparty/cpm-libwebm.cmake b/thirdparty/cpm-libwebm.cmake new file mode 100644 index 000000000..0b6bf4f0a --- /dev/null +++ b/thirdparty/cpm-libwebm.cmake @@ -0,0 +1,31 @@ +CPMAddPackage( + NAME libwebm + VERSION 1.0.0.29 + URL "https://chromium.googlesource.com/webm/libwebm/+archive/2f9fc054ab9547ca06071ec68dab9d54960abb2e.tar.gz" + EXCLUDE_FROM_ALL ON + DOWNLOAD_ONLY YES +) + +if(libwebm_ADDED) + set(libwebm_SOURCES + + common/file_util.cc + common/file_util.h + common/hdr_util.cc + common/hdr_util.h + common/webmids.h + + mkvmuxer/mkvmuxer.cc + mkvmuxer/mkvmuxer.h + mkvmuxer/mkvmuxertypes.h + mkvmuxer/mkvmuxerutil.cc + mkvmuxer/mkvmuxerutil.h + mkvmuxer/mkvwriter.cc + mkvmuxer/mkvwriter.h + ) + list(TRANSFORM libwebm_SOURCES PREPEND "${libwebm_SOURCE_DIR}/") + add_library(webm STATIC ${libwebm_SOURCES}) + target_include_directories(webm PUBLIC "${libwebm_SOURCE_DIR}") + target_compile_features(webm PRIVATE cxx_std_11) + add_library(webm::libwebm ALIAS webm) +endif() diff --git a/thirdparty/cpm-libyuv.cmake b/thirdparty/cpm-libyuv.cmake new file mode 100644 index 000000000..30cc925d6 --- /dev/null +++ b/thirdparty/cpm-libyuv.cmake @@ -0,0 +1,77 @@ +CPMAddPackage( + NAME libyuv + VERSION 0 + URL "https://chromium.googlesource.com/libyuv/libyuv/+archive/b2528b0be934de1918e20c85fc170d809eeb49ab.tar.gz" + EXCLUDE_FROM_ALL ON + DOWNLOAD_ONLY YES +) + +if(libyuv_ADDED) + set(libyuv_SOURCES + + # Headers + include/libyuv.h + include/libyuv/basic_types.h + include/libyuv/compare.h + include/libyuv/convert.h + include/libyuv/convert_argb.h + include/libyuv/convert_from.h + include/libyuv/convert_from_argb.h + include/libyuv/cpu_id.h + include/libyuv/mjpeg_decoder.h + include/libyuv/planar_functions.h + include/libyuv/rotate.h + include/libyuv/rotate_argb.h + include/libyuv/rotate_row.h + include/libyuv/row.h + include/libyuv/scale.h + include/libyuv/scale_argb.h + include/libyuv/scale_rgb.h + include/libyuv/scale_row.h + include/libyuv/scale_uv.h + include/libyuv/version.h + include/libyuv/video_common.h + + # Source Files + source/compare.cc + source/compare_common.cc + source/compare_gcc.cc + source/compare_win.cc + source/convert.cc + source/convert_argb.cc + source/convert_from.cc + source/convert_from_argb.cc + source/convert_jpeg.cc + source/convert_to_argb.cc + source/convert_to_i420.cc + source/cpu_id.cc + source/mjpeg_decoder.cc + source/mjpeg_validate.cc + source/planar_functions.cc + source/rotate.cc + source/rotate_any.cc + source/rotate_argb.cc + source/rotate_common.cc + source/rotate_gcc.cc + source/rotate_win.cc + source/row_any.cc + source/row_common.cc + source/row_gcc.cc + source/row_win.cc + source/scale.cc + source/scale_any.cc + source/scale_argb.cc + source/scale_common.cc + source/scale_gcc.cc + source/scale_rgb.cc + source/scale_uv.cc + source/scale_win.cc + source/video_common.cc + ) + list(TRANSFORM libyuv_SOURCES PREPEND "${libyuv_SOURCE_DIR}/") + add_library(yuv STATIC ${libyuv_SOURCES}) + + target_include_directories(yuv PUBLIC "${libyuv_SOURCE_DIR}/include") + + add_library(libyuv::libyuv ALIAS yuv) +endif() From 61198a46e733b12d7770f766b4d3a972b0eb9829 Mon Sep 17 00:00:00 2001 From: James R Date: Sat, 28 Jan 2023 22:27:37 -0800 Subject: [PATCH 19/38] Add libvorbis (and ogg) dependency --- CMakeLists.txt | 2 ++ cmake/Modules/FindVorbis.cmake | 33 +++++++++++++++++++++++++++++++ cmake/Modules/FindVorbisEnc.cmake | 33 +++++++++++++++++++++++++++++++ src/CMakeLists.txt | 1 + thirdparty/CMakeLists.txt | 2 ++ thirdparty/cpm-libvorbis.cmake | 11 +++++++++++ thirdparty/cpm-ogg.cmake | 6 ++++++ 7 files changed, 88 insertions(+) create mode 100644 cmake/Modules/FindVorbis.cmake create mode 100644 cmake/Modules/FindVorbisEnc.cmake create mode 100644 thirdparty/cpm-libvorbis.cmake create mode 100644 thirdparty/cpm-ogg.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index de4b96ceb..455c4b0b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,8 @@ if("${SRB2_CONFIG_SYSTEM_LIBRARIES}") find_package(CURL REQUIRED) find_package(GME REQUIRED) find_package(VPX REQUIRED) + find_package(Vorbis REQUIRED) + find_package(VorbisEnc REQUIRED) endif() if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR}) diff --git a/cmake/Modules/FindVorbis.cmake b/cmake/Modules/FindVorbis.cmake new file mode 100644 index 000000000..c24a57ca2 --- /dev/null +++ b/cmake/Modules/FindVorbis.cmake @@ -0,0 +1,33 @@ +include(LibFindMacros) + +libfind_pkg_check_modules(Vorbis_PKGCONF Vorbis) + +find_path(Vorbis_INCLUDE_DIR + NAMES vorbis/codec.h + PATHS + ${Vorbis_PKGCONF_INCLUDE_DIRS} + "/usr/include" + "/usr/local/include" +) + +find_library(Vorbis_LIBRARY + NAMES vorbis + PATHS + ${Vorbis_PKGCONF_LIBRARY_DIRS} + "/usr/lib" + "/usr/local/lib" +) + +set(Vorbis_PROCESS_INCLUDES Vorbis_INCLUDE_DIR) +set(Vorbis_PROCESS_LIBS Vorbis_LIBRARY) +libfind_process(Vorbis) + +if(Vorbis_FOUND AND NOT TARGET Vorbis::vorbis) + add_library(Vorbis::vorbis UNKNOWN IMPORTED) + set_target_properties( + Vorbis::vorbis + PROPERTIES + IMPORTED_LOCATION "${Vorbis_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Vorbis_INCLUDE_DIR}" + ) +endif() diff --git a/cmake/Modules/FindVorbisEnc.cmake b/cmake/Modules/FindVorbisEnc.cmake new file mode 100644 index 000000000..f429001fc --- /dev/null +++ b/cmake/Modules/FindVorbisEnc.cmake @@ -0,0 +1,33 @@ +include(LibFindMacros) + +libfind_pkg_check_modules(VorbisEnc_PKGCONF VorbisEnc) + +find_path(VorbisEnc_INCLUDE_DIR + NAMES vorbis/vorbisenc.h + PATHS + ${VorbisEnc_PKGCONF_INCLUDE_DIRS} + "/usr/include" + "/usr/local/include" +) + +find_library(VorbisEnc_LIBRARY + NAMES vorbisenc + PATHS + ${VorbisEnc_PKGCONF_LIBRARY_DIRS} + "/usr/lib" + "/usr/local/lib" +) + +set(VorbisEnc_PROCESS_INCLUDES VorbisEnc_INCLUDE_DIR) +set(VorbisEnc_PROCESS_LIBS VorbisEnc_LIBRARY) +libfind_process(VorbisEnc) + +if(VorbisEnc_FOUND AND NOT TARGET Vorbis::vorbisenc) + add_library(Vorbis::vorbisenc UNKNOWN IMPORTED) + set_target_properties( + Vorbis::vorbisenc + PROPERTIES + IMPORTED_LOCATION "${VorbisEnc_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${VorbisEnc_INCLUDE_DIR}" + ) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7cd3db33d..8ff8942d5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -231,6 +231,7 @@ target_link_libraries(SRB2SDL2 PRIVATE fmt) target_link_libraries(SRB2SDL2 PRIVATE imgui::imgui) target_link_libraries(SRB2SDL2 PRIVATE webm::libwebm webm::libvpx) target_link_libraries(SRB2SDL2 PRIVATE libyuv::libyuv) +target_link_libraries(SRB2SDL2 PRIVATE Vorbis::vorbis Vorbis::vorbisenc) target_link_libraries(SRB2SDL2 PRIVATE acsvm) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index d4cad6bba..46e2b2a3d 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -16,6 +16,8 @@ include("cpm-sdl2.cmake") include("cpm-curl.cmake") include("cpm-libgme.cmake") include("cpm-libvpx.cmake") + include("cpm-ogg.cmake") # libvorbis depends + include("cpm-libvorbis.cmake") endif() include("cpm-rapidjson.cmake") diff --git a/thirdparty/cpm-libvorbis.cmake b/thirdparty/cpm-libvorbis.cmake new file mode 100644 index 000000000..59bc11b3c --- /dev/null +++ b/thirdparty/cpm-libvorbis.cmake @@ -0,0 +1,11 @@ +CPMAddPackage( + NAME vorbis + VERSION 1.3.7 + URL "https://github.com/xiph/vorbis/releases/download/v1.3.7/libvorbis-1.3.7.zip" + EXCLUDE_FROM_ALL ON +) + +if(vorbis_ADDED) + add_library(Vorbis::vorbis ALIAS vorbis) + add_library(Vorbis::vorbisenc ALIAS vorbisenc) +endif() diff --git a/thirdparty/cpm-ogg.cmake b/thirdparty/cpm-ogg.cmake new file mode 100644 index 000000000..7f5f6123d --- /dev/null +++ b/thirdparty/cpm-ogg.cmake @@ -0,0 +1,6 @@ +CPMAddPackage( + NAME ogg + VERSION 1.3.5 + URL "https://github.com/xiph/ogg/releases/download/v1.3.5/libogg-1.3.5.zip" + EXCLUDE_FROM_ALL ON +) From 8ee785bb7e5dd2c814217325e25963669aa6b4a6 Mon Sep 17 00:00:00 2001 From: James R Date: Wed, 22 Feb 2023 20:35:02 -0800 Subject: [PATCH 20/38] cpm-ogg.cmake: fix libvorbis cross-build Vorbis couldn't find ogg so tell it explicitly where it is. --- thirdparty/cpm-ogg.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/thirdparty/cpm-ogg.cmake b/thirdparty/cpm-ogg.cmake index 7f5f6123d..cb37cf0b6 100644 --- a/thirdparty/cpm-ogg.cmake +++ b/thirdparty/cpm-ogg.cmake @@ -4,3 +4,10 @@ CPMAddPackage( URL "https://github.com/xiph/ogg/releases/download/v1.3.5/libogg-1.3.5.zip" EXCLUDE_FROM_ALL ON ) + +if(ogg_ADDED) + # Fixes bug with find_package not being able to find + # ogg when cross-building. + set(OGG_INCLUDE_DIR "${ogg_SOURCE_DIR}/include") + set(OGG_LIBRARY Ogg:ogg) +endif() From 3b5245f9748b5afc08d5b0373329a92745b625f0 Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:03:23 -0800 Subject: [PATCH 21/38] Add basic multimedia container and encoder interfaces Adds the media subdirectory. --- src/CMakeLists.txt | 1 + src/media/CMakeLists.txt | 7 +++++ src/media/audio_encoder.hpp | 39 +++++++++++++++++++++++ src/media/container.hpp | 53 +++++++++++++++++++++++++++++++ src/media/encoder.hpp | 51 ++++++++++++++++++++++++++++++ src/media/video_encoder.hpp | 58 ++++++++++++++++++++++++++++++++++ src/media/video_frame.hpp | 63 +++++++++++++++++++++++++++++++++++++ 7 files changed, 272 insertions(+) create mode 100644 src/media/CMakeLists.txt create mode 100644 src/media/audio_encoder.hpp create mode 100644 src/media/container.hpp create mode 100644 src/media/encoder.hpp create mode 100644 src/media/video_encoder.hpp create mode 100644 src/media/video_frame.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8ff8942d5..ada3a3f4f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -549,6 +549,7 @@ if(SRB2_CONFIG_ENABLE_TESTS) add_subdirectory(tests) endif() add_subdirectory(menus) +add_subdirectory(media) # strip debug symbols into separate file when using gcc. # to be consistent with Makefile, don't generate for OS X. diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt new file mode 100644 index 000000000..f0a7575bd --- /dev/null +++ b/src/media/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(SRB2SDL2 PRIVATE + audio_encoder.hpp + container.hpp + encoder.hpp + video_encoder.hpp + video_frame.hpp +) diff --git a/src/media/audio_encoder.hpp b/src/media/audio_encoder.hpp new file mode 100644 index 000000000..3aa9f5cf5 --- /dev/null +++ b/src/media/audio_encoder.hpp @@ -0,0 +1,39 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_AUDIO_ENCODER_HPP__ +#define __SRB2_MEDIA_AUDIO_ENCODER_HPP__ + +#include + +#include "encoder.hpp" + +namespace srb2::media +{ + +class AudioEncoder : virtual public MediaEncoder +{ +public: + using sample_buffer_t = tcb::span; + + struct Config + { + int channels; + int sample_rate; + }; + + virtual void encode(sample_buffer_t samples) = 0; + + virtual int channels() const = 0; + virtual int sample_rate() const = 0; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_AUDIO_ENCODER_HPP__ diff --git a/src/media/container.hpp b/src/media/container.hpp new file mode 100644 index 000000000..590d496f7 --- /dev/null +++ b/src/media/container.hpp @@ -0,0 +1,53 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_CONTAINER_HPP__ +#define __SRB2_MEDIA_CONTAINER_HPP__ + +#include +#include +#include +#include + +#include "audio_encoder.hpp" +#include "video_encoder.hpp" + +namespace srb2::media +{ + +class MediaContainer +{ +public: + using dtor_cb_t = std::function; + using time_unit_t = std::chrono::duration; + + struct Config + { + std::string file_name; + dtor_cb_t destructor_callback; + }; + + virtual ~MediaContainer() = default; + + virtual std::unique_ptr make_audio_encoder(AudioEncoder::Config config) = 0; + virtual std::unique_ptr make_video_encoder(VideoEncoder::Config config) = 0; + + virtual const char* name() const = 0; + virtual const char* file_name() const = 0; + + // These are normally estimates. However, when called from + // Config::destructor_callback, these are the exact final + // values. + virtual time_unit_t duration() const = 0; + virtual std::size_t size() const = 0; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_CONTAINER_HPP__ diff --git a/src/media/encoder.hpp b/src/media/encoder.hpp new file mode 100644 index 000000000..d6fa9c049 --- /dev/null +++ b/src/media/encoder.hpp @@ -0,0 +1,51 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_ENCODER_HPP__ +#define __SRB2_MEDIA_ENCODER_HPP__ + +#include +#include + +#include + +namespace srb2::media +{ + +class MediaEncoder +{ +public: + using time_unit_t = std::chrono::duration; + + struct BitRate + { + std::size_t bits; // 8 bits = 1 byte :) + time_unit_t period; + }; + + virtual ~MediaEncoder() = default; + + // Should be called finally but it's optional. + virtual void flush() = 0; + + virtual const char* name() const = 0; + + // Returns an average bit rate over a constant period of + // time, assuming no frames drops. + virtual BitRate estimated_bit_rate() const = 0; + +protected: + using frame_buffer_t = tcb::span; + + virtual void write_frame(frame_buffer_t frame, time_unit_t timestamp, bool is_key_frame) = 0; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_ENCODER_HPP__ diff --git a/src/media/video_encoder.hpp b/src/media/video_encoder.hpp new file mode 100644 index 000000000..1230bd8da --- /dev/null +++ b/src/media/video_encoder.hpp @@ -0,0 +1,58 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VIDEO_ENCODER_HPP__ +#define __SRB2_MEDIA_VIDEO_ENCODER_HPP__ + +#include "encoder.hpp" +#include "video_frame.hpp" + +namespace srb2::media +{ + +class VideoEncoder : virtual public MediaEncoder +{ +public: + struct Config + { + int width; + int height; + int frame_rate; + VideoFrame::BufferMethod buffer_method; + }; + + struct FrameCount + { + // Number of real frames, not counting frame skips. + int frames; + + time_unit_t duration; + }; + + // VideoFrame::width() and VideoFrame::height() should be + // used on the returned frame. + virtual VideoFrame::instance_t new_frame(int width, int height, int pts) = 0; + + virtual void encode(VideoFrame::instance_t frame) = 0; + + virtual int width() const = 0; + virtual int height() const = 0; + virtual int frame_rate() const = 0; + + // Reports the number of threads used, if the encoder is + // multithreaded. + virtual int thread_count() const = 0; + + // Number of frames fully encoded so far. + virtual FrameCount frame_count() const = 0; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_VIDEO_ENCODER_HPP__ diff --git a/src/media/video_frame.hpp b/src/media/video_frame.hpp new file mode 100644 index 000000000..acf278ff5 --- /dev/null +++ b/src/media/video_frame.hpp @@ -0,0 +1,63 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VIDEO_FRAME_HPP__ +#define __SRB2_MEDIA_VIDEO_FRAME_HPP__ + +#include +#include +#include + +#include + +namespace srb2::media +{ + +class VideoFrame +{ +public: + using instance_t = std::unique_ptr; + + enum class BufferMethod + { + // Returns an already allocated buffer for each + // frame. See VideoFrame::rgba_buffer(). The encoder + // completely manages allocating this buffer. + kEncoderAllocatedRGBA8888, + }; + + struct Buffer + { + tcb::span plane; + std::size_t row_stride; // size of each row + }; + + virtual int width() const = 0; + virtual int height() const = 0; + + int pts() const { return pts_; } + + // Returns a buffer that should be + // filled with RGBA pixels. + // + // This method may only be used if + // the encoder was configured with + // BufferMethod::kEncoderAllocatedRGBA8888. + virtual const Buffer& rgba_buffer() const = 0; + +protected: + VideoFrame(int pts) : pts_(pts) {} + +private: + int pts_; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_VIDEO_FRAME_HPP__ From e9f5a75d4a8d85db334dff9e72c8bb16393fff5f Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:05:25 -0800 Subject: [PATCH 22/38] media: add "options" cvar abstraction --- src/media/CMakeLists.txt | 2 + src/media/options.cpp | 118 +++++++++++++++++++++++++++++++++++++++ src/media/options.hpp | 51 +++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 src/media/options.cpp create mode 100644 src/media/options.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index f0a7575bd..fc4c6202a 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -2,6 +2,8 @@ target_sources(SRB2SDL2 PRIVATE audio_encoder.hpp container.hpp encoder.hpp + options.cpp + options.hpp video_encoder.hpp video_frame.hpp ) diff --git a/src/media/options.cpp b/src/media/options.cpp new file mode 100644 index 000000000..b933d9ad0 --- /dev/null +++ b/src/media/options.cpp @@ -0,0 +1,118 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include + +#include + +#include "../cxxutil.hpp" +#include "../m_fixed.h" +#include "options.hpp" + +using namespace srb2::media; + +static std::vector g_cvars; + +Options::Options(const char* prefix, map_t map) : prefix_(prefix), map_(map) +{ + for (auto& [suffix, cvar] : map_) + { + cvar.name = strdup(fmt::format("{}_{}", prefix_, suffix).c_str()); + g_cvars.emplace_back(&cvar); + } +} + +const consvar_t& Options::cvar(const char* option) const +{ + const consvar_t& cvar = map_.at(option); + + SRB2_ASSERT(cvar.string != nullptr); + + return cvar; +} + +template <> +int Options::get(const char* option) const +{ + return cvar(option).value; +} + +template <> +float Options::get(const char* option) const +{ + return FixedToFloat(cvar(option).value); +} + +static consvar_t range_cvar(const char* default_value, int32_t min, int32_t max, int32_t flags = 0) +{ + return CVAR_INIT( + nullptr, + default_value, + CV_SAVE | flags, + new CV_PossibleValue_t[] {{min, "MIN"}, {max, "MAX"}, {}}, + nullptr + ); +} + +template <> +consvar_t Options::range(const char* default_value, float min, float max) +{ + return range_cvar(default_value, FloatToFixed(min), FloatToFixed(max), CV_FLOAT); +} + +template <> +consvar_t Options::range_min(const char* default_value, float min) +{ + return range_cvar(default_value, FloatToFixed(min), INT32_MAX); +} + +template <> +consvar_t Options::range(const char* default_value, int min, int max) +{ + return range_cvar(default_value, min, max); +} + +template <> +consvar_t Options::range_min(const char* default_value, int min) +{ + return range_cvar(default_value, min, INT32_MAX); +} + +template <> +consvar_t Options::value_map(const char* default_value, std::map values) +{ + auto* arr = new CV_PossibleValue_t[values.size() + 1]; + + std::size_t i = 0; + + for (const auto& [k, v] : values) + { + arr[i].value = v; + arr[i].strvalue = k; + + i++; + } + + arr[i].value = 0; + arr[i].strvalue = nullptr; + + return CVAR_INIT(nullptr, default_value, CV_SAVE, arr, nullptr); +} + +void srb2::media::register_options() +{ + for (auto cvar : g_cvars) + { + CV_RegisterVar(cvar); + } + + g_cvars = {}; +} diff --git a/src/media/options.hpp b/src/media/options.hpp new file mode 100644 index 000000000..d7ce7fd2a --- /dev/null +++ b/src/media/options.hpp @@ -0,0 +1,51 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_OPTIONS_HPP__ +#define __SRB2_MEDIA_OPTIONS_HPP__ + +#include +#include + +#include "../command.h" + +namespace srb2::media +{ + +class Options +{ +public: + using map_t = std::unordered_map; + + Options(const char* prefix, map_t map); + + template + T get(const char* option) const; + + template + static consvar_t range(const char* default_value, T min, T max); + + template + static consvar_t range_min(const char* default_value, T min); + + template + static consvar_t value_map(const char* default_value, std::map values); + +private: + const char* prefix_; + map_t map_; + + const consvar_t& cvar(const char* option) const; +}; + +void register_options(); + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_OPTIONS_HPP__ From 650264ea86f39d0e1232046babcd6e0004254d6b Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:07:45 -0800 Subject: [PATCH 23/38] media: add libvorbis encoder --- src/media/CMakeLists.txt | 3 + src/media/vorbis.cpp | 138 +++++++++++++++++++++++++++++++++++++ src/media/vorbis.hpp | 54 +++++++++++++++ src/media/vorbis_error.hpp | 54 +++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 src/media/vorbis.cpp create mode 100644 src/media/vorbis.hpp create mode 100644 src/media/vorbis_error.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index fc4c6202a..1ecf75d21 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -6,4 +6,7 @@ target_sources(SRB2SDL2 PRIVATE options.hpp video_encoder.hpp video_frame.hpp + vorbis.cpp + vorbis.hpp + vorbis_error.hpp ) diff --git a/src/media/vorbis.cpp b/src/media/vorbis.cpp new file mode 100644 index 000000000..6b33aa22e --- /dev/null +++ b/src/media/vorbis.cpp @@ -0,0 +1,138 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include + +#include +#include + +#include "../cxxutil.hpp" +#include "vorbis.hpp" +#include "vorbis_error.hpp" + +using namespace srb2::media; + +// clang-format off +const Options VorbisEncoder::options_("vorbis", { + {"quality", Options::range("0", -0.1f, 1.f)}, + {"max_bitrate", Options::range_min("-1", -1)}, + {"nominal_bitrate", Options::range_min("-1", -1)}, + {"min_bitrate", Options::range_min("-1", -1)}, +}); +// clang-format on + +VorbisEncoder::VorbisEncoder(Config cfg) +{ + const long max_bitrate = options_.get("max_bitrate"); + const long nominal_bitrate = options_.get("nominal_bitrate"); + const long min_bitrate = options_.get("min_bitrate"); + + vorbis_info_init(&vi_); + + if (max_bitrate != -1 || nominal_bitrate != -1 || min_bitrate != -1) + { + // managed bitrate mode + VorbisError error = + vorbis_encode_init(&vi_, cfg.channels, cfg.sample_rate, max_bitrate, nominal_bitrate, min_bitrate); + + if (error != 0) + { + throw std::invalid_argument(fmt::format( + "vorbis_encode_init: {}, max_bitrate={}, nominal_bitrate={}, min_bitrate={}", + error, + max_bitrate, + nominal_bitrate, + min_bitrate + )); + } + } + else + { + // variable bitrate mode + const float quality = options_.get("quality"); + + VorbisError error = vorbis_encode_init_vbr(&vi_, cfg.channels, cfg.sample_rate, quality); + + if (error != 0) + { + throw std::invalid_argument(fmt::format("vorbis_encode_init: {}, quality={}", error, quality)); + } + } + + SRB2_ASSERT(vorbis_analysis_init(&vd_, &vi_) == 0); + SRB2_ASSERT(vorbis_block_init(&vd_, &vb_) == 0); +} + +VorbisEncoder::~VorbisEncoder() +{ + vorbis_block_clear(&vb_); + vorbis_dsp_clear(&vd_); + vorbis_info_clear(&vi_); +} + +VorbisEncoder::headers_t VorbisEncoder::generate_headers() +{ + headers_t op; + + vorbis_comment vc; + vorbis_comment_init(&vc); + + VorbisError error = vorbis_analysis_headerout(&vd_, &vc, &op[0], &op[1], &op[2]); + + if (error != 0) + { + throw std::invalid_argument(fmt::format("vorbis_analysis_headerout: {}", error)); + } + + vorbis_comment_clear(&vc); + + return op; +} + +void VorbisEncoder::analyse(sample_buffer_t in) +{ + const int ch = channels(); + + const std::size_t n = in.size() / ch; + float** fv = vorbis_analysis_buffer(&vd_, n); + + for (std::size_t i = 0; i < n; ++i) + { + auto s = in.subspan(i * ch, ch); + + fv[0][i] = s[0]; + fv[1][i] = s[1]; + } + + // automatically handles end of stream if n = 0 + SRB2_ASSERT(vorbis_analysis_wrote(&vd_, n) == 0); + + while (vorbis_analysis_blockout(&vd_, &vb_) > 0) + { + SRB2_ASSERT(vorbis_analysis(&vb_, nullptr) == 0); + SRB2_ASSERT(vorbis_bitrate_addblock(&vb_) == 0); + + ogg_packet op; + + while (vorbis_bitrate_flushpacket(&vd_, &op) > 0) + { + write_packet(&op); + } + } +} + +void VorbisEncoder::write_packet(ogg_packet* op) +{ + using T = const std::byte; + tcb::span p(reinterpret_cast(op->packet), static_cast(op->bytes)); + + write_frame(p, std::chrono::duration(vorbis_granule_time(&vd_, op->granulepos)), true); +} diff --git a/src/media/vorbis.hpp b/src/media/vorbis.hpp new file mode 100644 index 000000000..333f31f8e --- /dev/null +++ b/src/media/vorbis.hpp @@ -0,0 +1,54 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VORBIS_HPP__ +#define __SRB2_MEDIA_VORBIS_HPP__ + +#include + +#include + +#include "audio_encoder.hpp" +#include "options.hpp" + +namespace srb2::media +{ + +class VorbisEncoder : public AudioEncoder +{ +public: + static const Options options_; + + VorbisEncoder(Config config); + ~VorbisEncoder(); + + virtual void encode(sample_buffer_t samples) override final { analyse(samples); } + virtual void flush() override final { analyse(); } + + virtual const char* name() const override final { return "Vorbis"; } + virtual int channels() const override final { return vi_.channels; } + virtual int sample_rate() const override final { return vi_.rate; } + +protected: + using headers_t = std::array; + + headers_t generate_headers(); + +private: + vorbis_info vi_; + vorbis_dsp_state vd_; + vorbis_block vb_; + + void analyse(sample_buffer_t samples = {}); + void write_packet(ogg_packet* op); +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_VORBIS_HPP__ diff --git a/src/media/vorbis_error.hpp b/src/media/vorbis_error.hpp new file mode 100644 index 000000000..7c4f9d3d6 --- /dev/null +++ b/src/media/vorbis_error.hpp @@ -0,0 +1,54 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VORBIS_ERROR_HPP__ +#define __SRB2_MEDIA_VORBIS_ERROR_HPP__ + +#include + +#include +#include + +class VorbisError +{ +public: + VorbisError(int error) : error_(error) {} + + operator int() const { return error_; } + + std::string name() const + { + switch (error_) + { + case OV_EFAULT: + return "Internal error (OV_EFAULT)"; + case OV_EINVAL: + return "Invalid settings (OV_EINVAL)"; + case OV_EIMPL: + return "Invalid settings (OV_EIMPL)"; + default: + return fmt::format("error {}", error_); + } + } + +private: + int error_; +}; + +template <> +struct fmt::formatter : formatter +{ + template + auto format(const VorbisError& error, FormatContext& ctx) const + { + return formatter::format(error.name(), ctx); + } +}; + +#endif // __SRB2_MEDIA_VORBIS_ERROR_HPP__ From 14152541316a28a39a5f855294f09f4ce3cbf0f5 Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:09:35 -0800 Subject: [PATCH 24/38] media: add YUV420p module Converts RGBA image to YUV420p, useful for most video codecs. --- src/media/CMakeLists.txt | 2 + src/media/yuv420p.cpp | 124 +++++++++++++++++++++++++++++++++++++++ src/media/yuv420p.hpp | 74 +++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 src/media/yuv420p.cpp create mode 100644 src/media/yuv420p.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index 1ecf75d21..d0ade1566 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -9,4 +9,6 @@ target_sources(SRB2SDL2 PRIVATE vorbis.cpp vorbis.hpp vorbis_error.hpp + yuv420p.cpp + yuv420p.hpp ) diff --git a/src/media/yuv420p.cpp b/src/media/yuv420p.cpp new file mode 100644 index 000000000..bfc0fd7c5 --- /dev/null +++ b/src/media/yuv420p.cpp @@ -0,0 +1,124 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include + +#include +#include +#include + +#include "../cxxutil.hpp" +#include "yuv420p.hpp" + +using namespace srb2::media; + +bool YUV420pFrame::BufferRGBA::resize(int width, int height) +{ + if (width == width_ && height == height_) + { + return false; + } + + width_ = width; + height_ = height; + + row_stride = width * 4; + + const std::size_t new_size = row_stride * height; + + // Overallocate since the vector's alignment can't be + // easily controlled. This is not a significant waste. + vec_.resize(new_size + (kAlignment - 1)); + + void* p = vec_.data(); + std::size_t n = vec_.size(); + + SRB2_ASSERT(std::align(kAlignment, 1, p, n) != nullptr); + + plane = tcb::span(reinterpret_cast(p), new_size); + + return true; +} + +void YUV420pFrame::BufferRGBA::erase() +{ + std::fill(vec_.begin(), vec_.end(), 0); +} + +void YUV420pFrame::BufferRGBA::release() +{ + if (!vec_.empty()) + { + *this = {}; + } +} + +const VideoFrame::Buffer& YUV420pFrame::rgba_buffer() const +{ + return *rgba_; +} + +void YUV420pFrame::convert() const +{ + // ABGR = RGBA in memory + libyuv::ABGRToI420( + rgba_->plane.data(), + rgba_->row_stride, + y_.plane.data(), + y_.row_stride, + u_.plane.data(), + u_.row_stride, + v_.plane.data(), + v_.row_stride, + width(), + height() + ); +} + +void YUV420pFrame::scale(const BufferRGBA& scaled_rgba) +{ + int vw = scaled_rgba.width(); + int vh = scaled_rgba.height(); + + uint8_t* p = scaled_rgba.plane.data(); + + const float ru = width() / static_cast(height()); + const float rs = vw / static_cast(vh); + + // Maintain aspect ratio of unscaled. Fit inside scaled + // aspect by centering image. + + if (rs > ru) // scaled is wider + { + vw = vh * ru; + p += (scaled_rgba.width() - vw) / 2 * 4; + } + else + { + vh = vw / ru; + p += (scaled_rgba.height() - vh) / 2 * scaled_rgba.row_stride; + } + + // Curiously, this function doesn't care about channel order. + libyuv::ARGBScale( + rgba_->plane.data(), + rgba_->row_stride, + width(), + height(), + p, + scaled_rgba.row_stride, + vw, + vh, + libyuv::FilterMode::kFilterNone + ); + + rgba_ = &scaled_rgba; +} diff --git a/src/media/yuv420p.hpp b/src/media/yuv420p.hpp new file mode 100644 index 000000000..f8be45e06 --- /dev/null +++ b/src/media/yuv420p.hpp @@ -0,0 +1,74 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_YUV420P_HPP__ +#define __SRB2_MEDIA_YUV420P_HPP__ + +#include +#include + +#include "video_frame.hpp" + +namespace srb2::media +{ + +class YUV420pFrame : public VideoFrame +{ +public: + // 32-byte aligned for AVX optimizations (see libyuv) + static constexpr int kAlignment = 32; + + class BufferRGBA : public VideoFrame::Buffer + { + public: + bool resize(int width, int height); // true if resized + + void erase(); // fills with black + void release(); + + int width() const { return width_; } + int height() const { return height_; } + + private: + int width_, height_; + + std::vector vec_; + }; + + YUV420pFrame(int pts, Buffer y, Buffer u, Buffer v, const BufferRGBA& rgba) : + VideoFrame(pts), y_(y), u_(u), v_(v), rgba_(&rgba) + { + } + + ~YUV420pFrame() = default; + + // Simply resets PTS and RGBA buffer while keeping YUV + // buffers intact. + void reset(int pts, const BufferRGBA& rgba) { *this = YUV420pFrame(pts, y_, u_, v_, rgba); } + + // Converts RGBA buffer to YUV planes. + void convert() const; + + // Scales the existing buffer into a new one. This new + // buffer replaces the existing one. + void scale(const BufferRGBA& rgba); + + virtual int width() const override { return rgba_->width(); } + virtual int height() const override { return rgba_->height(); } + + virtual const Buffer& rgba_buffer() const override; + +private: + Buffer y_, u_, v_; + const BufferRGBA* rgba_; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_YUV420P_HPP__ From b8015b4ad2fd2a3f296f507f5287ff662351d7eb Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:08:38 -0800 Subject: [PATCH 25/38] media: add libvpx VP8 encoder --- src/media/CMakeLists.txt | 3 + src/media/vp8.cpp | 293 +++++++++++++++++++++++++++++++++++++++ src/media/vp8.hpp | 102 ++++++++++++++ src/media/vpx_error.hpp | 45 ++++++ 4 files changed, 443 insertions(+) create mode 100644 src/media/vp8.cpp create mode 100644 src/media/vp8.hpp create mode 100644 src/media/vpx_error.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index d0ade1566..3e748c08a 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -9,6 +9,9 @@ target_sources(SRB2SDL2 PRIVATE vorbis.cpp vorbis.hpp vorbis_error.hpp + vp8.cpp + vp8.hpp + vpx_error.hpp yuv420p.cpp yuv420p.hpp ) diff --git a/src/media/vp8.cpp b/src/media/vp8.cpp new file mode 100644 index 000000000..341b1b932 --- /dev/null +++ b/src/media/vp8.cpp @@ -0,0 +1,293 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../cxxutil.hpp" +#include "vp8.hpp" +#include "vpx_error.hpp" +#include "yuv420p.hpp" + +using namespace srb2::media; + +namespace +{ + +namespace KeyFrameOption +{ + +enum : int +{ + kAuto = -1, +}; + +}; // namespace KeyFrameOption + +namespace DeadlineOption +{ + +enum : int +{ + kInfinite = 0, +}; + +}; // namespace DeadlineOption + +}; // namespace + +// clang-format off +const Options VP8Encoder::options_("vp8", { + {"quality_mode", Options::value_map("q", { + {"vbr", VPX_VBR}, + {"cbr", VPX_CBR}, + {"cq", VPX_CQ}, + {"q", VPX_Q}, + })}, + {"target_bitrate", Options::range_min("800", 1)}, + {"min_q", Options::range("4", 4, 63)}, + {"max_q", Options::range("55", 4, 63)}, + {"kf_min", Options::range_min("0", 0)}, + {"kf_max", Options::value_map("auto", { + {"auto", KeyFrameOption::kAuto}, + {"MIN", 0}, + {"MAX", INT32_MAX}, + })}, + {"cpu_used", Options::range("0", -16, 16)}, + {"cq_level", Options::range("10", 0, 63)}, + {"deadline", Options::value_map("10", { + {"infinite", DeadlineOption::kInfinite}, + {"MIN", 1}, + {"MAX", INT32_MAX}, + })}, + {"sharpness", Options::range("7", 0, 7)}, + {"token_parts", Options::range("0", 0, 3)}, + {"threads", Options::range_min("1", 1)}, +}); +// clang-format on + +vpx_codec_iface_t* VP8Encoder::kCodec = vpx_codec_vp8_cx(); + +const vpx_codec_enc_cfg_t VP8Encoder::configure(const Config user) +{ + vpx_codec_enc_cfg_t cfg; + vpx_codec_enc_config_default(kCodec, &cfg, 0); + + cfg.g_threads = options_.get("threads"); + + cfg.g_w = user.width; + cfg.g_h = user.height; + + cfg.g_bit_depth = VPX_BITS_8; + cfg.g_input_bit_depth = 8; + + cfg.g_timebase.num = 1; + cfg.g_timebase.den = user.frame_rate; + + cfg.g_pass = VPX_RC_ONE_PASS; + cfg.rc_end_usage = static_cast(options_.get("quality_mode")); + cfg.kf_mode = VPX_KF_AUTO; + + cfg.rc_target_bitrate = options_.get("target_bitrate"); + cfg.rc_min_quantizer = options_.get("min_q"); + cfg.rc_max_quantizer = options_.get("max_q"); + + // Keyframe spacing, in number of frames. + // kf_max_dist should be low enough to allow scrubbing. + + int kf_max = options_.get("kf_max"); + + if (kf_max == KeyFrameOption::kAuto) + { + // Automatically pick a good rate + kf_max = (user.frame_rate / 2); // every .5s + } + + cfg.kf_min_dist = options_.get("kf_min"); + cfg.kf_max_dist = kf_max; + + return cfg; +} + +VP8Encoder::VP8Encoder(Config config) : ctx_(config), img_(config.width, config.height), frame_rate_(config.frame_rate) +{ + SRB2_ASSERT(config.buffer_method == VideoFrame::BufferMethod::kEncoderAllocatedRGBA8888); + + control(VP8E_SET_CPUUSED, "cpu_used"); + control(VP8E_SET_CQ_LEVEL, "cq_level"); + control(VP8E_SET_SHARPNESS, "sharpness"); + control(VP8E_SET_TOKEN_PARTITIONS, "token_parts"); + + auto plane = [this](int k, int ycs = 0) + { + using T = uint8_t; + auto view = tcb::span(reinterpret_cast(img_->planes[k]), img_->stride[k] * (img_->h >> ycs)); + + return VideoFrame::Buffer {view, static_cast(img_->stride[k])}; + }; + + frame_ = std::make_unique( + 0, + plane(VPX_PLANE_Y), + plane(VPX_PLANE_U, img_->y_chroma_shift), + plane(VPX_PLANE_V, img_->y_chroma_shift), + rgba_buffer_ + ); +} + +VP8Encoder::CtxWrapper::CtxWrapper(const Config user) +{ + const vpx_codec_enc_cfg_t cfg = configure(user); + + if (vpx_codec_enc_init(&ctx_, kCodec, &cfg, 0) != VPX_CODEC_OK) + { + throw std::invalid_argument(fmt::format("vpx_codec_enc_init: {}", VpxError(ctx_))); + } +} + +VP8Encoder::CtxWrapper::~CtxWrapper() +{ + vpx_codec_destroy(&ctx_); +} + +VP8Encoder::ImgWrapper::ImgWrapper(int width, int height) +{ + SRB2_ASSERT(vpx_img_alloc(&img_, VPX_IMG_FMT_I420, width, height, YUV420pFrame::kAlignment) != nullptr); +} + +VP8Encoder::ImgWrapper::~ImgWrapper() +{ + vpx_img_free(&img_); +} + +VideoFrame::instance_t VP8Encoder::new_frame(int width, int height, int pts) +{ + SRB2_ASSERT(frame_ != nullptr); + + if (rgba_buffer_.resize(width, height)) + { + // If there was a resize, the aspect ratio may not + // match. When the frame is scaled later, it will be + // "fit" into the target aspect ratio, leaving some + // empty space around the scaled image. (See + // VP8Encoder::encode) + // + // Set whole scaled buffer to black now so the empty + // space appears as "black bars". + rgba_scaled_buffer_.erase(); + } + + frame_->reset(pts, rgba_buffer_); + + return std::move(frame_); +} + +void VP8Encoder::encode(VideoFrame::instance_t frame) +{ + { + using T = YUV420pFrame; + + SRB2_ASSERT(frame_ == nullptr); + SRB2_ASSERT(dynamic_cast(frame.get()) != nullptr); + + frame_ = std::unique_ptr(static_cast(frame.release())); + } + + // This frame must be scaled to match encoder configuration + if (frame_->width() != width() || frame_->height() != height()) + { + rgba_scaled_buffer_.resize(width(), height()); + frame_->scale(rgba_scaled_buffer_); + } + else + { + rgba_scaled_buffer_.release(); + } + + frame_->convert(); + + if (vpx_codec_encode(ctx_, img_, frame_->pts(), 1, 0, deadline_) != VPX_CODEC_OK) + { + throw std::invalid_argument(fmt::format("VP8Encoder::encode: vpx_codec_encode: {}", VpxError(ctx_))); + } + + process(); +} + +void VP8Encoder::flush() +{ + do + { + if (vpx_codec_encode(ctx_, nullptr, 0, 0, 0, 0) != VPX_CODEC_OK) + { + throw std::invalid_argument(fmt::format("VP8Encoder::flush: vpx_codec_encode: {}", VpxError(ctx_))); + } + } while (process()); +} + +bool VP8Encoder::process() +{ + bool output = false; + + vpx_codec_iter_t iter = NULL; + const vpx_codec_cx_pkt_t* pkt; + + while ((pkt = vpx_codec_get_cx_data(ctx_, &iter))) + { + output = true; + + if (pkt->kind != VPX_CODEC_CX_FRAME_PKT) + { + continue; + } + + auto& frame = pkt->data.frame; + + { + const std::lock_guard _(frame_count_mutex_); + + duration_ = frame.pts + frame.duration; + frame_count_++; + } + + const float ts = frame.pts / static_cast(frame_rate()); + + using T = const std::byte; + tcb::span p(reinterpret_cast(frame.buf), frame.sz); + + write_frame(p, std::chrono::duration(ts), (frame.flags & VPX_FRAME_IS_KEY)); + } + + return output; +} + +template +void VP8Encoder::control(vp8e_enc_control_id id, const char* option) +{ + auto value = options_.get(option); + + if (vpx_codec_control_(ctx_, id, value) != VPX_CODEC_OK) + { + throw std::invalid_argument(fmt::format("vpx_codec_control: {}, {}={}", VpxError(ctx_), option, value)); + } +} + +VideoEncoder::FrameCount VP8Encoder::frame_count() const +{ + const std::lock_guard _(frame_count_mutex_); + + return {frame_count_, std::chrono::duration(duration_ / static_cast(frame_rate()))}; +} diff --git a/src/media/vp8.hpp b/src/media/vp8.hpp new file mode 100644 index 000000000..b4c68f24d --- /dev/null +++ b/src/media/vp8.hpp @@ -0,0 +1,102 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VP8_HPP__ +#define __SRB2_MEDIA_VP8_HPP__ + +#include + +#include + +#include "options.hpp" +#include "video_encoder.hpp" +#include "yuv420p.hpp" + +namespace srb2::media +{ + +class VP8Encoder : public VideoEncoder +{ +public: + static const Options options_; + + VP8Encoder(VideoEncoder::Config config); + + virtual VideoFrame::instance_t new_frame(int width, int height, int pts) override final; + + virtual void encode(VideoFrame::instance_t frame) override final; + virtual void flush() override final; + + virtual const char* name() const override final { return "VP8"; } + virtual int width() const override final { return img_->w; } + virtual int height() const override final { return img_->h; } + virtual int frame_rate() const override final { return frame_rate_; } + virtual int thread_count() const override final { return thread_count_; } + + virtual FrameCount frame_count() const override final; + +private: + class CtxWrapper + { + public: + CtxWrapper(const Config config); + ~CtxWrapper(); + + operator vpx_codec_ctx_t*() { return &ctx_; } + operator vpx_codec_ctx_t&() { return ctx_; } + + private: + vpx_codec_ctx_t ctx_; + }; + + class ImgWrapper + { + public: + ImgWrapper(int width, int height); + ~ImgWrapper(); + + operator vpx_image_t*() { return &img_; } + vpx_image_t* operator->() { return &img_; } + const vpx_image_t* operator->() const { return &img_; } + + private: + vpx_image_t img_; + }; + + static vpx_codec_iface_t* kCodec; + + static const vpx_codec_enc_cfg_t configure(const Config config); + + CtxWrapper ctx_; + ImgWrapper img_; + + const int frame_rate_; + const int thread_count_ = options_.get("threads"); + const int deadline_ = options_.get("deadline"); + + mutable std::recursive_mutex frame_count_mutex_; + + int duration_ = 0; + int frame_count_ = 0; + + YUV420pFrame::BufferRGBA // + rgba_buffer_, + rgba_scaled_buffer_; // only allocated if input NEEDS scaling + + std::unique_ptr frame_; + + bool process(); + + template // T = option type + void control(vp8e_enc_control_id id, const char* option); +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_VP8_HPP__ diff --git a/src/media/vpx_error.hpp b/src/media/vpx_error.hpp new file mode 100644 index 000000000..06b502398 --- /dev/null +++ b/src/media/vpx_error.hpp @@ -0,0 +1,45 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_VPX_ERROR_HPP__ +#define __SRB2_MEDIA_VPX_ERROR_HPP__ + +#include + +#include +#include + +class VpxError +{ +public: + VpxError(vpx_codec_ctx_t& ctx) : ctx_(&ctx) {} + + std::string description() const + { + const char* error = vpx_codec_error(ctx_); + const char* detail = vpx_codec_error_detail(ctx_); + + return detail ? fmt::format("{}: {}", error, detail) : error; + } + +private: + vpx_codec_ctx_t* ctx_; +}; + +template <> +struct fmt::formatter : formatter +{ + template + auto format(const VpxError& error, FormatContext& ctx) const + { + return formatter::format(error.description(), ctx); + } +}; + +#endif // __SRB2_MEDIA_VPX_ERROR_HPP__ From 60899133c1711d6d4dca6c6766a7b75f3d95fa3e Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:12:55 -0800 Subject: [PATCH 26/38] media: add libwebm container --- src/media/CMakeLists.txt | 6 + src/media/cfile.cpp | 34 ++++++ src/media/cfile.hpp | 36 ++++++ src/media/webm.hpp | 26 ++++ src/media/webm_container.cpp | 228 +++++++++++++++++++++++++++++++++++ src/media/webm_container.hpp | 112 +++++++++++++++++ src/media/webm_writer.hpp | 32 +++++ 7 files changed, 474 insertions(+) create mode 100644 src/media/cfile.cpp create mode 100644 src/media/cfile.hpp create mode 100644 src/media/webm.hpp create mode 100644 src/media/webm_container.cpp create mode 100644 src/media/webm_container.hpp create mode 100644 src/media/webm_writer.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index 3e748c08a..5025a7550 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -1,5 +1,7 @@ target_sources(SRB2SDL2 PRIVATE audio_encoder.hpp + cfile.cpp + cfile.hpp container.hpp encoder.hpp options.cpp @@ -12,6 +14,10 @@ target_sources(SRB2SDL2 PRIVATE vp8.cpp vp8.hpp vpx_error.hpp + webm.hpp + webm_container.cpp + webm_container.hpp + webm_writer.hpp yuv420p.cpp yuv420p.hpp ) diff --git a/src/media/cfile.cpp b/src/media/cfile.cpp new file mode 100644 index 000000000..25c8ba019 --- /dev/null +++ b/src/media/cfile.cpp @@ -0,0 +1,34 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include +#include + +#include + +#include "cfile.hpp" + +using namespace srb2::media; + +CFile::CFile(const std::string file_name) : name_(file_name) +{ + file_ = std::fopen(name(), "wb"); + + if (file_ == nullptr) + { + throw std::invalid_argument(fmt::format("{}: {}", name(), std::strerror(errno))); + } +} + +CFile::~CFile() +{ + std::fclose(file_); +} diff --git a/src/media/cfile.hpp b/src/media/cfile.hpp new file mode 100644 index 000000000..7d0e8595e --- /dev/null +++ b/src/media/cfile.hpp @@ -0,0 +1,36 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_CFILE_HPP__ +#define __SRB2_MEDIA_CFILE_HPP__ + +#include +#include + +namespace srb2::media +{ + +class CFile +{ +public: + CFile(const std::string file_name); + ~CFile(); + + operator std::FILE*() const { return file_; } + + const char* name() const { return name_.c_str(); } + +private: + std::string name_; + std::FILE* file_; +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_CFILE_HPP__ diff --git a/src/media/webm.hpp b/src/media/webm.hpp new file mode 100644 index 000000000..2031f58f0 --- /dev/null +++ b/src/media/webm.hpp @@ -0,0 +1,26 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_WEBM_HPP__ +#define __SRB2_MEDIA_WEBM_HPP__ + +#include +#include +#include + +namespace srb2::media::webm +{ + +using track = uint64_t; +using timestamp = uint64_t; +using duration = std::chrono::duration; + +}; // namespace srb2::media::webm + +#endif // __SRB2_MEDIA_WEBM_HPP__ diff --git a/src/media/webm_container.cpp b/src/media/webm_container.cpp new file mode 100644 index 000000000..3d2ffc5b9 --- /dev/null +++ b/src/media/webm_container.cpp @@ -0,0 +1,228 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include + +#include "../cxxutil.hpp" +#include "webm_vorbis.hpp" +#include "webm_vp8.hpp" + +using namespace srb2::media; + +using time_unit_t = MediaEncoder::time_unit_t; + +WebmContainer::WebmContainer(const Config cfg) : writer_(cfg.file_name), dtor_cb_(cfg.destructor_callback) +{ + SRB2_ASSERT(segment_.Init(&writer_) == true); +} + +WebmContainer::~WebmContainer() +{ + flush_queue(); + + if (!segment_.Finalize()) + { + CONS_Alert(CONS_WARNING, "mkvmuxer::Segment::Finalize has failed\n"); + } + + finalized_ = true; + + if (dtor_cb_) + { + dtor_cb_(*this); + } +} + +std::unique_ptr WebmContainer::make_audio_encoder(AudioEncoder::Config cfg) +{ + const uint64_t tid = segment_.AddAudioTrack(cfg.sample_rate, cfg.channels, 0); + + return std::make_unique(*this, tid, cfg); +} + +std::unique_ptr WebmContainer::make_video_encoder(VideoEncoder::Config cfg) +{ + const uint64_t tid = segment_.AddVideoTrack(cfg.width, cfg.height, 0); + + return std::make_unique(*this, tid, cfg); +} + +time_unit_t WebmContainer::duration() const +{ + if (finalized_) + { + const auto& si = *segment_.segment_info(); + + return webm::duration(static_cast(si.duration() * si.timecode_scale())); + } + + auto _ = queue_guard(); + + return webm::duration(latest_timestamp_); +} + +std::size_t WebmContainer::size() const +{ + if (finalized_) + { + return writer_.Position(); + } + + auto _ = queue_guard(); + + return writer_.Position() + queue_size_; +} + +std::size_t WebmContainer::track_size(webm::track trackid) const +{ + auto _ = queue_guard(); + + return queue_.at(trackid).data_size; +} + +time_unit_t WebmContainer::track_duration(webm::track trackid) const +{ + auto _ = queue_guard(); + + return webm::duration(queue_.at(trackid).flushed_timestamp); +} + +void WebmContainer::write_frame( + tcb::span buffer, + webm::track trackid, + webm::timestamp timestamp, + bool is_key_frame +) +{ + SRB2_ASSERT( + segment_.AddFrame( + reinterpret_cast(buffer.data()), + buffer.size_bytes(), + trackid, + timestamp, + is_key_frame + ) == true + ); + + queue_[trackid].data_size += buffer.size_bytes(); +} + +void WebmContainer::queue_frame( + tcb::span buffer, + webm::track trackid, + webm::timestamp timestamp, + bool is_key_frame +) +{ + auto _ = queue_guard(); + + auto& q = queue_.at(trackid); + + // If another track is behind this one, queue this + // frame until the other track catches up. + + if (flush_queue() < timestamp) + { + q.frames.emplace_back(buffer, timestamp, is_key_frame); + queue_size_ += buffer.size_bytes(); + } + else + { + // Nothing is waiting; this frame can be written + // immediately. + + write_frame(buffer, trackid, timestamp, is_key_frame); + q.flushed_timestamp = timestamp; + } + + q.queued_timestamp = timestamp; + latest_timestamp_ = timestamp; +} + +webm::timestamp WebmContainer::flush_queue() +{ + webm::timestamp goal = latest_timestamp_; + + // Flush all tracks' queues, not beyond the end of the + // shortest track. + + for (const auto& [_, q] : queue_) + { + if (q.queued_timestamp < goal) + { + goal = q.queued_timestamp; + } + } + + webm::timestamp shortest; + + do + { + shortest = goal; + + for (const auto& [tid, q] : queue_) + { + const webm::timestamp flushed = flush_single_queue(tid, q.queued_timestamp); + + if (flushed < shortest) + { + shortest = flushed; + } + } + } while (shortest < goal); + + return shortest; +} + +webm::timestamp WebmContainer::flush_single_queue(webm::track trackid, webm::timestamp flushed_timestamp) +{ + webm::timestamp goal = flushed_timestamp; + + // Find the lowest timestamp yet flushed from all other + // tracks. We cannot write a frame beyond this timestamp + // because PTS must only increase. + + for (const auto& [tid, other] : queue_) + { + if (tid != trackid && other.flushed_timestamp < goal) + { + goal = other.flushed_timestamp; + } + } + + auto& q = queue_.at(trackid); + auto it = q.frames.cbegin(); + + // Flush previously queued frames in this track. + + for (; it != q.frames.cend(); ++it) + { + const auto& frame = *it; + + if (frame.timestamp > goal) + { + q.flushed_timestamp = frame.timestamp; + break; + } + + write_frame(frame.buffer, trackid, frame.timestamp, frame.is_key_frame); + + queue_size_ -= frame.buffer.size(); + } + + q.frames.erase(q.frames.cbegin(), it); + + if (q.frames.empty()) + { + q.flushed_timestamp = flushed_timestamp; + } + + return goal; +} diff --git a/src/media/webm_container.hpp b/src/media/webm_container.hpp new file mode 100644 index 000000000..e763515c8 --- /dev/null +++ b/src/media/webm_container.hpp @@ -0,0 +1,112 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_WEBM_CONTAINER_HPP__ +#define __SRB2_MEDIA_WEBM_CONTAINER_HPP__ + +#include +#include +#include +#include + +#include + +#include "container.hpp" +#include "webm.hpp" +#include "webm_writer.hpp" + +namespace srb2::media +{ + +class WebmContainer : virtual public MediaContainer +{ +public: + WebmContainer(Config cfg); + ~WebmContainer(); + + virtual std::unique_ptr make_audio_encoder(AudioEncoder::Config config) override final; + virtual std::unique_ptr make_video_encoder(VideoEncoder::Config config) override final; + + virtual const char* name() const override final { return "WebM"; } + virtual const char* file_name() const override final { return writer_.name(); } + + virtual time_unit_t duration() const override final; + virtual std::size_t size() const override final; + + std::size_t track_size(webm::track trackid) const; + time_unit_t track_duration(webm::track trackid) const; + + template + T* get_track(webm::track trackid) const + { + return reinterpret_cast(segment_.GetTrackByNumber(trackid)); + } + + void init_queue(webm::track trackid) { queue_.try_emplace(trackid); } + + // init_queue MUST be called before using this function. + void queue_frame( + tcb::span buffer, + webm::track trackid, + webm::timestamp timestamp, + bool is_key_frame + ); + + auto queue_guard() const { return std::lock_guard(queue_mutex_); } + +private: + struct FrameQueue + { + struct Frame + { + std::vector buffer; + webm::timestamp timestamp; + bool is_key_frame; + + Frame(tcb::span buffer_, webm::timestamp timestamp_, bool is_key_frame_) : + buffer(buffer_.begin(), buffer_.end()), timestamp(timestamp_), is_key_frame(is_key_frame_) + { + } + }; + + std::vector frames; + std::size_t data_size = 0; + + webm::timestamp flushed_timestamp = 0; + webm::timestamp queued_timestamp = 0; + }; + + mkvmuxer::Segment segment_; + WebmWriter writer_; + + mutable std::recursive_mutex queue_mutex_; + + std::unordered_map queue_; + + webm::timestamp latest_timestamp_ = 0; + std::size_t queue_size_ = 0; + + bool finalized_ = false; + const dtor_cb_t dtor_cb_; + + void write_frame( + tcb::span buffer, + webm::track trackid, + webm::timestamp timestamp, + bool is_key_frame + ); + + // Returns the largest timestamp that can be written. + webm::timestamp flush_queue(); + webm::timestamp flush_single_queue(webm::track trackid, webm::timestamp flushed_timestamp); +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_WEBM_CONTAINER_HPP__ diff --git a/src/media/webm_writer.hpp b/src/media/webm_writer.hpp new file mode 100644 index 000000000..50e091baa --- /dev/null +++ b/src/media/webm_writer.hpp @@ -0,0 +1,32 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_WEBM_WRITER_HPP__ +#define __SRB2_MEDIA_WEBM_WRITER_HPP__ + +#include +#include + +#include + +#include "cfile.hpp" + +namespace srb2::media +{ + +class WebmWriter : public CFile, public mkvmuxer::MkvWriter +{ +public: + WebmWriter(const std::string file_name) : CFile(file_name), MkvWriter(static_cast(*this)) {} + ~WebmWriter() { MkvWriter::Close(); } +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_WEBM_WRITER_HPP__ From 654f97fa72e2e301ab32f2626ea8ca26c540a278 Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:15:23 -0800 Subject: [PATCH 27/38] media: add WebM Vorbis and VP8 encoders --- src/media/CMakeLists.txt | 4 ++ src/media/webm_encoder.hpp | 51 ++++++++++++++++++++++ src/media/webm_vorbis.hpp | 59 +++++++++++++++++++++++++ src/media/webm_vorbis_lace.cpp | 79 ++++++++++++++++++++++++++++++++++ src/media/webm_vp8.hpp | 44 +++++++++++++++++++ 5 files changed, 237 insertions(+) create mode 100644 src/media/webm_encoder.hpp create mode 100644 src/media/webm_vorbis.hpp create mode 100644 src/media/webm_vorbis_lace.cpp create mode 100644 src/media/webm_vp8.hpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index 5025a7550..8282d847f 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -15,8 +15,12 @@ target_sources(SRB2SDL2 PRIVATE vp8.hpp vpx_error.hpp webm.hpp + webm_encoder.hpp webm_container.cpp webm_container.hpp + webm_vorbis.hpp + webm_vorbis_lace.cpp + webm_vp8.hpp webm_writer.hpp yuv420p.cpp yuv420p.hpp diff --git a/src/media/webm_encoder.hpp b/src/media/webm_encoder.hpp new file mode 100644 index 000000000..197886dba --- /dev/null +++ b/src/media/webm_encoder.hpp @@ -0,0 +1,51 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_WEBM_ENCODER_HPP__ +#define __SRB2_MEDIA_WEBM_ENCODER_HPP__ + +#include + +#include "encoder.hpp" +#include "webm_container.hpp" + +namespace srb2::media +{ + +template +class WebmEncoder : virtual public MediaEncoder +{ +public: + WebmEncoder(WebmContainer& container, webm::track trackid) : container_(container), trackid_(trackid) + { + container_.init_queue(trackid_); + } + +protected: + WebmContainer& container_; + webm::track trackid_; + + std::size_t size() const { return container_.track_size(trackid_); } + time_unit_t duration() const { return container_.track_duration(trackid_); } + + static T* get_track(const WebmContainer& container, webm::track trackid) { return container.get_track(trackid); } + + T* track() const { return get_track(container_, trackid_); } + + virtual void write_frame(frame_buffer_t p, time_unit_t ts, bool is_key_frame) override final + { + const auto ts_nano = std::chrono::duration_cast(ts); + + container_.queue_frame(p, trackid_, ts_nano.count(), is_key_frame); + } +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_WEBM_ENCODER_HPP__ diff --git a/src/media/webm_vorbis.hpp b/src/media/webm_vorbis.hpp new file mode 100644 index 000000000..5e3825ceb --- /dev/null +++ b/src/media/webm_vorbis.hpp @@ -0,0 +1,59 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_WEBM_VORBIS_HPP__ +#define __SRB2_MEDIA_WEBM_VORBIS_HPP__ + +#include +#include +#include +#include + +#include "../cxxutil.hpp" +#include "vorbis.hpp" +#include "webm_encoder.hpp" + +namespace srb2::media +{ + +class WebmVorbisEncoder : public WebmEncoder, public VorbisEncoder +{ +public: + WebmVorbisEncoder(WebmContainer& container, webm::track trackid, AudioEncoder::Config cfg) : + WebmEncoder(container, trackid), VorbisEncoder(cfg) + { + // write Vorbis extra data + + const auto p = make_vorbis_private_data(); + + SRB2_ASSERT(track()->SetCodecPrivate(reinterpret_cast(p.data()), p.size()) == true); + } + + virtual BitRate estimated_bit_rate() const override final + { + auto _ = container_.queue_guard(); + + const std::chrono::duration t = duration(); + + if (t <= t.zero()) + { + return {}; + } + + using namespace std::chrono_literals; + return {static_cast((size() * 8) / t.count()), 1s}; + } + +private: + std::vector make_vorbis_private_data(); +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_WEBM_VORBIS_HPP__ diff --git a/src/media/webm_vorbis_lace.cpp b/src/media/webm_vorbis_lace.cpp new file mode 100644 index 000000000..506d3f763 --- /dev/null +++ b/src/media/webm_vorbis_lace.cpp @@ -0,0 +1,79 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include +#include + +#include + +#include "webm_vorbis.hpp" + +// https://www.matroska.org/technical/notes.html#xiph-lacing +// https://www.matroska.org/technical/codec_specs.html#a_vorbis + +using namespace srb2::media; + +static std::size_t lace_length(const ogg_packet& op) +{ + return (op.bytes / 255) + 1; +} + +static void lace(std::vector& v, const ogg_packet& op) +{ + // The lacing size is encoded in at least one byte. If + // the value is 255, add the value of the next byte in + // sequence. This ends with a byte that is less than 255. + + std::fill_n(std::back_inserter(v), lace_length(op) - 1, std::byte {255}); + + const unsigned char n = (op.bytes % 255); + v.emplace_back(std::byte {n}); +} + +std::vector WebmVorbisEncoder::make_vorbis_private_data() +{ + const headers_t packets = generate_headers(); + + std::vector v; + + // There are three Vorbis header packets. The lacing for + // these packets in Matroska does not count the final + // packet. + + // clang-format off + v.reserve( + 1 + + lace_length(packets[0]) + + lace_length(packets[1]) + + packets[0].bytes + + packets[1].bytes + + packets[2].bytes); + // clang-format on + + // The first byte is the number of packets. Once again, + // the last packet is not counted. + v.emplace_back(std::byte {2}); + + // Then the laced sizes for each packet. + lace(v, packets[0]); + lace(v, packets[1]); + + // Then each packet's data. The last packet's data + // actually is written here. + for (auto op : packets) + { + tcb::span p(reinterpret_cast(op.packet), op.bytes); + + std::copy(p.begin(), p.end(), std::back_inserter(v)); + } + + return v; +} diff --git a/src/media/webm_vp8.hpp b/src/media/webm_vp8.hpp new file mode 100644 index 000000000..44e951146 --- /dev/null +++ b/src/media/webm_vp8.hpp @@ -0,0 +1,44 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_WEBM_VP8_HPP__ +#define __SRB2_MEDIA_WEBM_VP8_HPP__ + +#include "vp8.hpp" +#include "webm_encoder.hpp" + +namespace srb2::media +{ + +class WebmVP8Encoder : public WebmEncoder, public VP8Encoder +{ +public: + WebmVP8Encoder(WebmContainer& container, webm::track trackid, VideoEncoder::Config cfg) : + WebmEncoder(container, trackid), VP8Encoder(cfg) + { + } + + virtual BitRate estimated_bit_rate() const override final + { + auto _ = container_.queue_guard(); + + const int frames = frame_count().frames; + + if (frames <= 0) + { + return {}; + } + + return {(size() * 8) / frames, std::chrono::duration(1.f / frame_rate())}; + } +}; + +}; // namespace srb2::media + +#endif // __SRB2_MEDIA_WEBM_VP8_HPP__ From 82251f6fb60653fbe6365ec2686ff31846cc1fc8 Mon Sep 17 00:00:00 2001 From: James R Date: Sun, 12 Feb 2023 02:28:30 -0800 Subject: [PATCH 28/38] media: add core AVRecorder Generic interface to audio and video encoders. --- src/media/CMakeLists.txt | 4 + src/media/avrecorder.cpp | 211 +++++++++++++++++++++++++++++++++ src/media/avrecorder.hpp | 76 ++++++++++++ src/media/avrecorder_impl.hpp | 164 +++++++++++++++++++++++++ src/media/avrecorder_queue.cpp | 142 ++++++++++++++++++++++ 5 files changed, 597 insertions(+) create mode 100644 src/media/avrecorder.cpp create mode 100644 src/media/avrecorder.hpp create mode 100644 src/media/avrecorder_impl.hpp create mode 100644 src/media/avrecorder_queue.cpp diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt index 8282d847f..ec387a428 100644 --- a/src/media/CMakeLists.txt +++ b/src/media/CMakeLists.txt @@ -1,5 +1,9 @@ target_sources(SRB2SDL2 PRIVATE audio_encoder.hpp + avrecorder.cpp + avrecorder.hpp + avrecorder_impl.hpp + avrecorder_queue.cpp cfile.cpp cfile.hpp container.hpp diff --git a/src/media/avrecorder.cpp b/src/media/avrecorder.cpp new file mode 100644 index 000000000..ef1962fed --- /dev/null +++ b/src/media/avrecorder.cpp @@ -0,0 +1,211 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../cxxutil.hpp" +#include "../i_time.h" +#include "../m_fixed.h" +#include "avrecorder_impl.hpp" +#include "webm_container.hpp" + +using namespace srb2::media; + +using Impl = AVRecorder::Impl; + +namespace +{ + +constexpr auto kBufferMethod = VideoFrame::BufferMethod::kEncoderAllocatedRGBA8888; + +}; // namespace + +Impl::Impl(Config cfg) : + max_size_(cfg.max_size), + max_duration_(cfg.max_duration), + + container_(std::make_unique(MediaContainer::Config { + .file_name = cfg.file_name, + })), + + audio_encoder_(make_audio_encoder(cfg)), + video_encoder_(make_video_encoder(cfg)), + + epoch_(I_GetTime()), + + thread_([this] { worker(); }) +{ +} + +std::unique_ptr Impl::make_audio_encoder(const Config cfg) const +{ + if (!cfg.audio) + { + return nullptr; + } + + const Config::Audio& a = *cfg.audio; + + return container_->make_audio_encoder({ + .channels = 2, + .sample_rate = a.sample_rate, + }); +} + +std::unique_ptr Impl::make_video_encoder(const Config cfg) const +{ + if (!cfg.video) + { + return nullptr; + } + + const Config::Video& v = *cfg.video; + + return container_->make_video_encoder({ + .width = v.width, + .height = v.height, + .frame_rate = v.frame_rate, + .buffer_method = kBufferMethod, + }); +} + +Impl::~Impl() +{ + valid_ = false; + wake_up_worker(); + thread_.join(); + + try + { + // Finally flush encoders, unless queues were finished + // already due to time or size constraints. + + if (!audio_queue_.finished()) + { + audio_encoder_->flush(); + } + + if (!video_queue_.finished()) + { + video_encoder_->flush(); + } + } + catch (const std::exception& ex) + { + CONS_Alert(CONS_ERROR, "AVRecorder::Impl::~Impl: %s\n", ex.what()); + return; + } +} + +std::optional Impl::advance_video_pts() +{ + auto _ = queue_guard(); + + // Don't let this queue grow out of hand. It's normal + // for encoding time to vary by a small margin and + // spend longer than one frame rate on a single + // frame. It should normalize though. + + if (video_queue_.vec_.size() >= 3) + { + return {}; + } + + SRB2_ASSERT(video_encoder_ != nullptr); + + const float tic_pts = video_encoder_->frame_rate() / static_cast(TICRATE); + const int pts = ((I_GetTime() - epoch_) + FixedToFloat(g_time.timefrac)) * tic_pts; + + if (!video_queue_.advance(pts, 1)) + { + return {}; + } + + return pts; +} + +void Impl::worker() +{ + for (;;) + { + QueueState qs; + + try + { + while ((qs = encode_queues()) == QueueState::kFlushed) + ; + } + catch (const std::exception& ex) + { + CONS_Alert(CONS_ERROR, "AVRecorder::Impl::worker: %s\n", ex.what()); + break; + } + + if (qs != QueueState::kFinished && valid_) + { + std::unique_lock lock(queue_mutex_); + + queue_cond_.wait(lock); + } + else + { + break; + } + } + + // Breaking out of the loop ensures invalidation! + valid_ = false; +} + +AVRecorder::AVRecorder(const Config config) : impl_(std::make_unique(config)) +{ +} + +AVRecorder::~AVRecorder() +{ + // impl_ is destroyed in a background thread so it doesn't + // block the thread AVRecorder was destroyed in. + // + // TODO: Save into a thread pool instead of detaching so + // the thread could be joined at program exit and + // not possibly terminate before fully destroyed? + + std::thread([_ = std::move(impl_)] {}).detach(); +} + +void AVRecorder::push_audio_samples(audio_buffer_t buffer) +{ + const auto _ = impl_->queue_guard(); + + auto& q = impl_->audio_queue_; + + if (!q.advance(q.pts(), buffer.size())) + { + return; + } + + using T = const float; + tcb::span p(reinterpret_cast(buffer.data()), buffer.size() * 2); // 2 channels + + std::copy(p.begin(), p.end(), std::back_inserter(q.vec_)); + + impl_->wake_up_worker(); +} + +bool AVRecorder::invalid() const +{ + return !impl_->valid_; +} diff --git a/src/media/avrecorder.hpp b/src/media/avrecorder.hpp new file mode 100644 index 000000000..45834ec08 --- /dev/null +++ b/src/media/avrecorder.hpp @@ -0,0 +1,76 @@ +// RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_MEDIA_AVRECORDER_HPP__ +#define __SRB2_MEDIA_AVRECORDER_HPP__ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../audio/sample.hpp" + +namespace srb2::media +{ + +class AVRecorder +{ +public: + using audio_sample_t = srb2::audio::Sample<2>; + using audio_buffer_t = tcb::span; + + class Impl; + + struct Config + { + struct Audio + { + int sample_rate; + }; + + struct Video + { + int width; + int height; + int frame_rate; + }; + + std::string file_name; + + std::optional max_size; // file size limit + std::optional> max_duration; + + std::optional