Merge remote-tracking branch 'origin/master' into special-stage-magician

This commit is contained in:
AJ Martinez 2023-02-25 03:23:14 -07:00
commit 17af9463ac
73 changed files with 4142 additions and 165 deletions

View file

@ -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
@ -55,3 +55,4 @@ SpacesInConditionalStatement: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
PenaltyReturnTypeOnItsOwnLine: 1000

View file

@ -132,6 +132,9 @@ if("${SRB2_CONFIG_SYSTEM_LIBRARIES}")
find_package(SDL2 REQUIRED)
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})

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -32,6 +32,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
m_aatree.c
m_anigif.c
m_argv.c
m_avrecorder.cpp
m_bbox.c
m_cheat.c
m_cond.c
@ -229,6 +230,9 @@ 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 Vorbis::vorbis Vorbis::vorbisenc)
target_link_libraries(SRB2SDL2 PRIVATE acsvm)
@ -546,6 +550,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.

View file

@ -62,6 +62,7 @@
#include "deh_tables.h"
#include "m_perfstats.h"
#include "k_specialstage.h"
#include "m_avrecorder.h"
#ifdef HAVE_DISCORDRPC
#include "discord.h"
@ -903,6 +904,7 @@ void D_RegisterClientCommands(void)
CV_RegisterVar(&cv_moviemode);
CV_RegisterVar(&cv_movie_option);
CV_RegisterVar(&cv_movie_folder);
M_AVRecorder_AddCommands();
// PNG variables
CV_RegisterVar(&cv_zlib_level);
CV_RegisterVar(&cv_zlib_memory);
@ -4942,23 +4944,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 +4992,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 +6474,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);
}

View file

@ -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;

View file

@ -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

View file

@ -4359,7 +4359,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.
@ -4369,6 +4369,7 @@ void G_LoadGameData(void)
UINT32 versionID;
UINT8 versionMinor;
UINT8 rtemp;
boolean gridunusable = false;
savebuffer_t save = {0};
//For records
@ -4423,6 +4424,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);
@ -4469,21 +4474,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);

View file

@ -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)
{

View file

@ -10,6 +10,7 @@
#include "../discord.h"
#endif
#include "../doomstat.h"
#include "../m_avrecorder.h"
#include "../st_stuff.h"
#include "../s_sound.h"
#include "../v_video.h"
@ -121,6 +122,8 @@ static void temp_legacy_finishupdate_draws()
}
if (cv_mindelay.value && consoleplayer == serverplayer && Playing())
SCR_DisplayLocalPing();
M_AVRecorder_DrawFrameRate();
}
if (marathonmode)

View file

@ -55,6 +55,10 @@ void I_StartupSound(void);
*/
void I_ShutdownSound(void);
/** \brief Update instance of AVRecorder for audio capture.
*/
void I_UpdateAudioRecorder(void);
/// ------------------------
/// SFX I/O
/// ------------------------

View file

@ -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

View file

@ -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];

View file

@ -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();
@ -2683,7 +2689,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;
@ -4080,10 +4086,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);
@ -7715,6 +7728,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--;

View file

@ -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 {
@ -1151,7 +1153,7 @@ extern struct challengesmenu_s {
SINT8 row, hilix, focusx;
UINT8 col, hiliy;
UINT8 *extradata;
challengegridextradata_t *extradata;
boolean pending;
boolean requestnew;

View file

@ -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
);
@ -4516,10 +4517,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;
fixed_t siz;
UINT8 *colormap = NULL, *bgmap = NULL;
fixed_t siz, accordion;
UINT8 id, num;
UINT32 edgelength;
boolean unlockedyet;
boolean categoryside;
id = (i * CHALLENGEGRIDHEIGHT) + j;
num = gamedata->challengegrid[id];
@ -4533,18 +4535,116 @@ 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))
// 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));
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;
goto drawborder;
}
if (ref->icon != NULL && ref->icon[0])
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_DrawStretchyFixedPatch(
(x*FRACUNIT) + (SHORT(pat->width)*(FRACUNIT-accordion)/2), y*FRACUNIT,
accordion,
FRACUNIT,
0, pat,
bgmap
);
pat = missingpat;
categoryside = (challengesmenu.extradata[id].flip <= TILEFLIP_MAX/4
|| challengesmenu.extradata[id].flip > (3*TILEFLIP_MAX)/4);
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)
@ -4552,58 +4652,110 @@ 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;
}
}
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_DrawStretchyFixedPatch(
((x + 5)*FRACUNIT) + (32*(FRACUNIT-accordion)/2), (y + 5)*FRACUNIT,
FixedDiv(32*accordion, siz),
FixedDiv(32 << FRACBITS, siz),
0, pat,
colormap
);
}
else
{
V_DrawStretchyFixedPatch(
((x + 2)*FRACUNIT) + (16*(FRACUNIT-accordion)/2), (y + 2)*FRACUNIT,
FixedDiv(16*accordion, siz),
FixedDiv(16 << FRACBITS, siz),
0, pat,
colormap
);
}
drawborder:
if (!hili)
@ -4611,12 +4763,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)
@ -4819,6 +4982,9 @@ static void M_DrawChallengePreview(INT32 x, INT32 y)
}
}
#define challengetransparentstrength 8
#define challengesgridstep 22
void M_DrawChallenges(void)
{
INT32 x = currentMenu->x, explodex, selectx;
@ -4828,8 +4994,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)
@ -4838,43 +5021,45 @@ void M_DrawChallenges(void)
goto challengedesc;
}
x -= 16;
V_DrawFadeFill(0, y-2, BASEVIDWIDTH, 90, 0, 31, challengetransparentstrength);
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)
if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j].flags & CHE_DONTDRAW)
{
continue;
}
@ -4887,7 +5072,7 @@ void M_DrawChallenges(void)
M_DrawChallengeTile(i, j, x, y, false);
}
x -= 16;
x -= challengesgridstep;
i--;
if (challengegridloops && i < 0)
{
@ -4903,7 +5088,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);
@ -4922,6 +5107,12 @@ challengedesc:
{
y = 120;
V_DrawScaledPatch(0, y,
(10-challengetransparentstrength)<<V_ALPHASHIFT,
W_CachePatchName("MENUHINT", PU_CACHE));
V_DrawFadeFill(0, y+27, BASEVIDWIDTH, BASEVIDHEIGHT - (y+27), 0, 31, challengetransparentstrength);
if (challengesmenu.currentunlock < MAXUNLOCKABLES)
{
str = unlockables[challengesmenu.currentunlock].name;
@ -4953,7 +5144,7 @@ challengedesc:
&& challengesmenu.currentunlock < MAXUNLOCKABLES
&& ((gamedata->unlocked[challengesmenu.currentunlock] == true)
|| ((challengesmenu.extradata != NULL)
&& (challengesmenu.extradata[i] & CHE_HINT))
&& (challengesmenu.extradata[i].flags & CHE_HINT))
)
)
{
@ -4961,6 +5152,9 @@ challengedesc:
}
}
#undef challengetransparentstrength
#undef challengesgridstep
// Statistics menu
#define STATSSTEP 10

View file

@ -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);

View file

@ -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")) {

252
src/m_avrecorder.cpp Normal file
View file

@ -0,0 +1,252 @@
// 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 <algorithm>
#include <chrono>
#include <cstdint>
#include <exception>
#include <memory>
#include <stdexcept>
#include <fmt/format.h>
#include <tcb/span.hpp>
#include "cxxutil.hpp"
#include "m_avrecorder.hpp"
#include "media/options.hpp"
#include "command.h"
#include "i_sound.h"
#include "m_avrecorder.h"
#include "m_fixed.h"
#include "screen.h" // vid global
#include "st_stuff.h" // st_palette
#include "v_video.h" // pLocalPalette
using namespace srb2::media;
namespace
{
namespace Res
{
// Using an unscoped enum here so it can implicitly cast to
// int (in CV_PossibleValue_t). Wrap this in a namespace so
// access is still scoped. E.g. Res::kGame
enum : int32_t
{
kGame, // user chosen resolution, vid.width
kBase, // smallest version maintaining aspect ratio, vid.width / vid.dupx
kBase2x,
kBase4x,
kWindow, // window size (monitor in fullscreen), vid.realwidth
kCustom, // movie_custom_resolution
};
}; // namespace Res
CV_PossibleValue_t movie_resolution_cons_t[] = {
{Res::kGame, "Native"},
{Res::kBase, "Small"},
{Res::kBase2x, "Medium"},
{Res::kBase4x, "Large"},
{Res::kWindow, "Window"},
{Res::kCustom, "Custom"},
{0, NULL}};
CV_PossibleValue_t movie_limit_cons_t[] = {{1, "MIN"}, {INT32_MAX, "MAX"}, {0, "Unlimited"}, {0, NULL}};
}; // namespace
consvar_t cv_movie_resolution = CVAR_INIT("movie_resolution", "Medium", CV_SAVE, movie_resolution_cons_t, NULL);
consvar_t cv_movie_custom_resolution = CVAR_INIT("movie_custom_resolution", "640x400", CV_SAVE, NULL, NULL);
consvar_t cv_movie_fps = CVAR_INIT("movie_fps", "60", CV_SAVE, CV_Natural, NULL);
consvar_t cv_movie_showfps = CVAR_INIT("movie_showfps", "Yes", CV_SAVE, CV_YesNo, NULL);
consvar_t cv_movie_sound = CVAR_INIT("movie_sound", "On", CV_SAVE, CV_OnOff, NULL);
consvar_t cv_movie_duration = CVAR_INIT("movie_duration", "Unlimited", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL);
consvar_t cv_movie_size = CVAR_INIT("movie_size", "8.0", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL);
std::shared_ptr<AVRecorder> g_av_recorder;
void M_AVRecorder_AddCommands(void)
{
CV_RegisterVar(&cv_movie_custom_resolution);
CV_RegisterVar(&cv_movie_duration);
CV_RegisterVar(&cv_movie_fps);
CV_RegisterVar(&cv_movie_resolution);
CV_RegisterVar(&cv_movie_showfps);
CV_RegisterVar(&cv_movie_size);
CV_RegisterVar(&cv_movie_sound);
srb2::media::Options::register_all();
}
static AVRecorder::Config configure()
{
AVRecorder::Config cfg {};
if (cv_movie_duration.value > 0)
{
cfg.max_duration = std::chrono::duration<float>(FixedToFloat(cv_movie_duration.value));
}
if (cv_movie_size.value > 0)
{
cfg.max_size = FixedToFloat(cv_movie_size.value) * 1024 * 1024;
}
if (sound_started && cv_movie_sound.value)
{
cfg.audio = {
.sample_rate = 44100,
};
}
cfg.video = {
.frame_rate = cv_movie_fps.value,
};
AVRecorder::Config::Video& v = *cfg.video;
auto basex = [&v](int scale)
{
v.width = vid.width / vid.dupx * scale;
v.height = vid.height / vid.dupy * scale;
};
switch (cv_movie_resolution.value)
{
case Res::kGame:
v.width = vid.width;
v.height = vid.height;
break;
case Res::kBase:
basex(1);
break;
case Res::kBase2x:
basex(2);
break;
case Res::kBase4x:
basex(4);
break;
case Res::kWindow:
v.width = vid.realwidth;
v.height = vid.realheight;
break;
case Res::kCustom:
if (sscanf(cv_movie_custom_resolution.string, "%dx%d", &v.width, &v.height) != 2)
{
throw std::invalid_argument(fmt::format(
"Bad movie_custom_resolution '{}', should be <width>x<height> (e.g. 640x400)",
cv_movie_custom_resolution.string
));
}
break;
default:
SRB2_ASSERT(false);
}
return cfg;
}
boolean M_AVRecorder_Open(const char* filename)
{
try
{
AVRecorder::Config cfg = configure();
cfg.file_name = filename;
g_av_recorder = std::make_shared<AVRecorder>(cfg);
I_UpdateAudioRecorder();
return true;
}
catch (const std::exception& ex)
{
CONS_Alert(CONS_ERROR, "Exception starting video recorder: %s\n", ex.what());
return false;
}
}
void M_AVRecorder_Close(void)
{
g_av_recorder.reset();
I_UpdateAudioRecorder();
}
const char* M_AVRecorder_GetFileExtension(void)
{
return AVRecorder::file_extension();
}
const char* M_AVRecorder_GetCurrentFormat(void)
{
SRB2_ASSERT(g_av_recorder != nullptr);
return g_av_recorder->format_name();
}
void M_AVRecorder_PrintCurrentConfiguration(void)
{
SRB2_ASSERT(g_av_recorder != nullptr);
g_av_recorder->print_configuration();
}
boolean M_AVRecorder_IsExpired(void)
{
SRB2_ASSERT(g_av_recorder != nullptr);
return g_av_recorder->invalid();
}
void M_AVRecorder_DrawFrameRate(void)
{
if (!cv_movie_showfps.value || !g_av_recorder)
{
return;
}
g_av_recorder->draw_statistics();
}
// TODO: remove once hwr2 twodee is finished
void M_AVRecorder_CopySoftwareScreen(void)
{
SRB2_ASSERT(g_av_recorder != nullptr);
auto frame = g_av_recorder->new_indexed_video_frame(vid.width, vid.height);
if (!frame)
{
return;
}
tcb::span<RGBA_t> pal(&pLocalPalette[std::max(st_palette, 0) * 256], 256);
tcb::span<uint8_t> scr(screens[0], vid.width * vid.height);
std::copy(pal.begin(), pal.end(), frame->palette.begin());
std::copy(scr.begin(), scr.end(), frame->screen.begin());
g_av_recorder->push_indexed_video_frame(std::move(frame));
}

53
src/m_avrecorder.h Normal file
View file

@ -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 M_AVRECORDER_H
#define M_AVRECORDER_H
#include "typedef.h" // consvar_t
#ifdef __cplusplus
extern "C" {
#endif
void M_AVRecorder_AddCommands(void);
const char *M_AVRecorder_GetFileExtension(void);
// True if successully opened.
boolean M_AVRecorder_Open(const char *filename);
void M_AVRecorder_Close(void);
// Check whether AVRecorder is still valid. Call M_AVRecorder_Close if expired.
boolean M_AVRecorder_IsExpired(void);
const char *M_AVRecorder_GetCurrentFormat(void);
void M_AVRecorder_PrintCurrentConfiguration(void);
void M_AVRecorder_DrawFrameRate(void);
// TODO: remove once hwr2 twodee is finished
void M_AVRecorder_CopySoftwareScreen(void);
extern consvar_t
cv_movie_custom_resolution,
cv_movie_duration,
cv_movie_fps,
cv_movie_resolution,
cv_movie_showfps,
cv_movie_size,
cv_movie_sound;
#ifdef __cplusplus
}; // extern "C"
#endif
#endif/*M_AVRECORDER_H*/

19
src/m_avrecorder.hpp Normal file
View file

@ -0,0 +1,19 @@
// 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 __M_AVRECORDER_HPP__
#define __M_AVRECORDER_HPP__
#include <memory> // shared_ptr
#include "media/avrecorder.hpp"
extern std::shared_ptr<srb2::media::AVRecorder> g_av_recorder;
#endif // __M_AVRECORDER_HPP__

View file

@ -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.
//
@ -259,28 +260,34 @@ quickcheckagain:
}
}
UINT8 *M_ChallengeGridExtraData(void)
void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata)
{
UINT8 i, j, num, id, tempid, work;
UINT8 *extradata;
boolean idchange;
if (!gamedata->challengegrid)
if (gamedata->challengegrid == NULL)
{
return NULL;
return;
}
extradata = Z_Malloc(
(gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)),
PU_STATIC, NULL);
if (!extradata)
if (extradata == NULL)
{
I_Error("M_ChallengeGridExtraData: was not able to allocate extradata");
return;
}
//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++)
@ -289,8 +296,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)
{
@ -304,13 +309,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)
{
@ -327,14 +332,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;
}
}
@ -356,11 +361,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
@ -368,13 +373,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;
}
@ -391,7 +396,7 @@ UINT8 *M_ChallengeGridExtraData(void)
}
else if (work < MAXUNLOCKABLES && gamedata->unlocked[work])
{
extradata[id] = CHE_HINT;
extradata[id].flags = CHE_HINT;
continue;
}
}
@ -414,14 +419,12 @@ UINT8 *M_ChallengeGridExtraData(void)
}
else if (work < MAXUNLOCKABLES && gamedata->unlocked[work])
{
extradata[id] = CHE_HINT;
extradata[id].flags = CHE_HINT;
continue;
}
}
}
}
return extradata;
}
void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2)
@ -909,7 +912,7 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud)
{
if (loud)
{
S_StartSound(NULL, sfx_ncitem);
S_StartSound(NULL, sfx_achiev);
}
return true;
}

View file

@ -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.
//
@ -138,7 +139,7 @@ typedef enum
#define MAXEMBLEMS 512
#define MAXUNLOCKABLES MAXCONDITIONSETS
#define CHALLENGEGRIDHEIGHT 5
#define CHALLENGEGRIDHEIGHT 4
#ifdef DEVELOP
#define CHALLENGEGRIDLOOPWIDTH 3
#else
@ -192,12 +193,21 @@ void M_NewGameDataStruct(void);
// Challenges menu stuff
void M_PopulateChallengeGrid(void);
UINT8 *M_ChallengeGridExtraData(void);
struct challengegridextradata_t
{
UINT8 flags;
UINT8 flip;
};
void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata);
#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

View file

@ -44,6 +44,7 @@
#include "command.h" // cv_execversion
#include "m_anigif.h"
#include "m_avrecorder.h"
// So that the screenshot menu auto-updates...
#include "k_menu.h"
@ -113,8 +114,8 @@ consvar_t cv_screenshot_folder = CVAR_INIT ("screenshot_folder", "", CV_SAVE, NU
consvar_t cv_screenshot_colorprofile = CVAR_INIT ("screenshot_colorprofile", "Yes", CV_SAVE, CV_YesNo, NULL);
static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {0, NULL}};
consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "GIF", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange);
static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {MM_AVRECORDER, "WebM"}, {0, NULL}};
consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "WebM", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange);
consvar_t cv_movie_option = CVAR_INIT ("movie_option", "Default", CV_SAVE|CV_CALL, screenshot_cons_t, Moviemode_option_Onchange);
consvar_t cv_movie_folder = CVAR_INIT ("movie_folder", "", CV_SAVE, NULL, NULL);
@ -1295,6 +1296,25 @@ static inline moviemode_t M_StartMovieGIF(const char *pathname)
}
#endif
static inline moviemode_t M_StartMovieAVRecorder(const char *pathname)
{
const char *ext = M_AVRecorder_GetFileExtension();
const char *freename;
if (!(freename = Newsnapshotfile(pathname, ext)))
{
CONS_Alert(CONS_ERROR, "Couldn't create %s file: no slots open in %s\n", ext, pathname);
return MM_OFF;
}
if (!M_AVRecorder_Open(va(pandf,pathname,freename)))
{
return MM_OFF;
}
return MM_AVRECORDER;
}
void M_StartMovie(void)
{
#if NUMSCREENS > 2
@ -1332,6 +1352,9 @@ void M_StartMovie(void)
case MM_SCREENSHOT:
moviemode = MM_SCREENSHOT;
break;
case MM_AVRECORDER:
moviemode = M_StartMovieAVRecorder(pathname);
break;
default: //???
return;
}
@ -1342,6 +1365,11 @@ void M_StartMovie(void)
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "GIF");
else if (moviemode == MM_SCREENSHOT)
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "screenshots");
else if (moviemode == MM_AVRECORDER)
{
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), M_AVRecorder_GetCurrentFormat());
M_AVRecorder_PrintCurrentConfiguration();
}
//singletics = (moviemode != MM_OFF);
#endif
@ -1353,6 +1381,22 @@ void M_SaveFrame(void)
// paranoia: should be unnecessary without singletics
static tic_t oldtic = 0;
if (moviemode == MM_AVRECORDER)
{
// TODO: replace once hwr2 twodee is finished
if (rendermode == render_soft)
{
M_AVRecorder_CopySoftwareScreen();
}
if (M_AVRecorder_IsExpired())
{
M_StopMovie();
}
return;
}
// skip interpolated frames for other modes
if (oldtic == I_GetTime())
return;
else
@ -1440,6 +1484,9 @@ void M_StopMovie(void)
#endif
case MM_SCREENSHOT:
break;
case MM_AVRECORDER:
M_AVRecorder_Close();
break;
default:
return;
}

View file

@ -29,7 +29,8 @@ typedef enum {
MM_OFF = 0,
MM_APNG,
MM_GIF,
MM_SCREENSHOT
MM_SCREENSHOT,
MM_AVRECORDER,
} moviemode_t;
extern moviemode_t moviemode;

34
src/media/CMakeLists.txt Normal file
View file

@ -0,0 +1,34 @@
target_sources(SRB2SDL2 PRIVATE
audio_encoder.hpp
avrecorder.cpp
avrecorder.hpp
avrecorder_feedback.cpp
avrecorder_impl.hpp
avrecorder_indexed.cpp
avrecorder_queue.cpp
cfile.cpp
cfile.hpp
container.hpp
encoder.hpp
options.cpp
options.hpp
options_values.cpp
video_encoder.hpp
video_frame.hpp
vorbis.cpp
vorbis.hpp
vorbis_error.hpp
vp8.cpp
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
)

View file

@ -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 <tcb/span.hpp>
#include "encoder.hpp"
namespace srb2::media
{
class AudioEncoder : virtual public MediaEncoder
{
public:
using sample_buffer_t = tcb::span<const float>;
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__

214
src/media/avrecorder.cpp Normal file
View file

@ -0,0 +1,214 @@
// 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 <algorithm>
#include <chrono>
#include <exception>
#include <iterator>
#include <memory>
#include <mutex>
#include <thread>
#include <utility>
#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<WebmContainer>(MediaContainer::Config {
cfg.file_name,
[this](const MediaContainer& container) { container_dtor_handler(container); },
})),
audio_encoder_(make_audio_encoder(cfg)),
video_encoder_(make_video_encoder(cfg)),
epoch_(I_GetTime()),
thread_([this] { worker(); })
{
}
std::unique_ptr<AudioEncoder> Impl::make_audio_encoder(const Config cfg) const
{
if (!cfg.audio)
{
return nullptr;
}
const Config::Audio& a = *cfg.audio;
return container_->make_audio_encoder({2, a.sample_rate});
}
std::unique_ptr<VideoEncoder> Impl::make_video_encoder(const Config cfg) const
{
if (!cfg.video)
{
return nullptr;
}
const Config::Video& v = *cfg.video;
return container_->make_video_encoder({v.width, v.height, v.frame_rate, 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<int> 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<float>(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;
}
const char* AVRecorder::file_extension()
{
return "webm";
}
AVRecorder::AVRecorder(const Config config) : impl_(std::make_unique<Impl>(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();
}
const char* AVRecorder::format_name() const
{
return impl_->container_->name();
}
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<T> p(reinterpret_cast<T*>(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_;
}

108
src/media/avrecorder.hpp Normal file
View file

@ -0,0 +1,108 @@
// 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 <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <tcb/span.hpp>
#include "../audio/sample.hpp"
namespace srb2::media
{
class AVRecorder
{
public:
using audio_sample_t = srb2::audio::Sample<2>;
using audio_buffer_t = tcb::span<const audio_sample_t>;
class Impl;
struct Config
{
struct Audio
{
int sample_rate;
};
struct Video
{
int width;
int height;
int frame_rate;
};
std::string file_name;
std::optional<std::size_t> max_size; // file size limit
std::optional<std::chrono::duration<float>> max_duration;
std::optional<Audio> audio;
std::optional<Video> video;
};
// TODO: remove once hwr2 twodee is finished
struct IndexedVideoFrame
{
using instance_t = std::unique_ptr<IndexedVideoFrame>;
std::array<RGBA_t, 256> palette;
std::vector<uint8_t> screen;
uint32_t width, height;
int pts;
IndexedVideoFrame(uint32_t width_, uint32_t height_, int pts_) :
screen(width_ * height_), width(width_), height(height_), pts(pts_)
{
}
};
// Returns the canonical file extension minus the dot.
// E.g. "webm" (not ".webm").
static const char* file_extension();
AVRecorder(Config config);
~AVRecorder();
void print_configuration() const;
void draw_statistics() const;
void push_audio_samples(audio_buffer_t buffer);
// May return nullptr in case called between units of
// Config::frame_rate
IndexedVideoFrame::instance_t new_indexed_video_frame(uint32_t width, uint32_t height);
void push_indexed_video_frame(IndexedVideoFrame::instance_t frame);
// Proper name of the container format.
const char* format_name() const;
// True if this instance has terminated. Continuing to use
// this interface is useless and the object should be
// destructed immediately.
bool invalid() const;
private:
std::unique_ptr<Impl> impl_;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_AVRECORDER_HPP__

View file

@ -0,0 +1,149 @@
// 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 <filesystem>
#include <sstream>
#include <string>
#include <fmt/format.h>
#include "../cxxutil.hpp"
#include "avrecorder_impl.hpp"
#include "../v_video.h"
using namespace srb2::media;
using Impl = AVRecorder::Impl;
namespace
{
constexpr float kMb = 1024.f * 1024.f;
}; // namespace
void Impl::container_dtor_handler(const MediaContainer& container) const
{
// Note that because this method is called from
// container_'s destructor, any member variables declared
// after Impl::container_ should not be accessed by now
// (since they would have already destructed).
if (max_size_ && container.size() > *max_size_)
{
const std::string line = fmt::format(
"Video size has exceeded limit {} > {} ({}%)."
" This should not happen, please report this bug.\n",
container.size(),
*max_size_,
100.f * (*max_size_ / static_cast<float>(container.size()))
);
CONS_Alert(CONS_WARNING, "%s\n", line.c_str());
}
std::ostringstream msg;
msg << "Video saved: " << std::filesystem::path(container.file_name()).filename().string()
<< fmt::format(" ({:.2f}", container.size() / kMb);
if (max_size_)
{
msg << fmt::format("/{:.2f}", *max_size_ / kMb);
}
msg << fmt::format(" MB, {:.1f}", container.duration().count());
if (max_duration_config_)
{
msg << fmt::format("/{:.1f}", max_duration_config_->count());
}
msg << " seconds)";
CONS_Printf("%s\n", msg.str().c_str());
}
void AVRecorder::print_configuration() const
{
if (impl_->audio_encoder_)
{
const auto& a = *impl_->audio_encoder_;
CONS_Printf("Audio: %s %dch %d Hz\n", a.name(), a.channels(), a.sample_rate());
}
if (impl_->video_encoder_)
{
const auto& v = *impl_->video_encoder_;
CONS_Printf(
"Video: %s %dx%d %d fps %d threads\n",
v.name(),
v.width(),
v.height(),
v.frame_rate(),
v.thread_count()
);
}
}
void AVRecorder::draw_statistics() const
{
SRB2_ASSERT(impl_->video_encoder_ != nullptr);
auto draw = [](int x, std::string text, int32_t flags = 0)
{
V_DrawThinString(
x,
190,
(V_6WIDTHSPACE | V_ALLOWLOWERCASE | V_SNAPTOBOTTOM | V_SNAPTORIGHT) | flags,
text.c_str()
);
};
const float fps = impl_->video_frame_rate_avg_;
const float size = impl_->container_->size();
const int32_t fps_color = [&]
{
const int cap = impl_->video_encoder_->frame_rate();
// red when dropped below 60% of the target
if (fps > 0.f && fps < (0.6f * cap))
{
return V_REDMAP;
}
return 0;
}();
const int32_t mb_color = [&]
{
if (!impl_->max_size_)
{
return 0;
}
const std::size_t cap = *impl_->max_size_;
// yellow when within 1 MB of the limit
if (size >= (cap - kMb))
{
return V_YELLOWMAP;
}
return 0;
}();
draw(200, fmt::format("{:.0f}", fps), fps_color);
draw(230, fmt::format("{:.1f}s", impl_->container_->duration().count()));
draw(260, fmt::format("{:.1f} MB", size / kMb), mb_color);
}

View file

@ -0,0 +1,175 @@
// 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_IMPL_HPP__
#define __SRB2_MEDIA_AVRECORDER_IMPL_HPP__
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
#include <vector>
#include "../i_time.h"
#include "avrecorder.hpp"
#include "container.hpp"
namespace srb2::media
{
class AVRecorder::Impl
{
public:
template <typename T>
class Queue
{
public:
// The use of typename = void is a GCC bug.
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85282
// Explicit specialization inside of a class still
// does not work as of 12.2.1.
template <typename, typename = void>
struct Traits
{
};
template <typename _>
struct Traits<AudioEncoder, _>
{
using frame_type = float;
};
template <typename _>
struct Traits<VideoEncoder, _>
{
using frame_type = IndexedVideoFrame::instance_t;
};
std::vector<typename Traits<T>::frame_type> vec_;
// This number only decrements once a frame has
// actually been written to container.
std::size_t queued_frames_ = 0;
Queue(const std::unique_ptr<T>& encoder, Impl& impl) : encoder_(encoder.get()), impl_(&impl) {}
// This method handles validation of the queue,
// finishing the queue and advancing PTS. Returns true
// if PTS was advanced.
bool advance(int pts, int duration);
// True if no more data may be queued.
bool finished() const { return finished_; }
// Presentation Time Stamp; one frame for video
// encoders, one sample for audio encoders.
int pts() const { return pts_; }
private:
using time_unit_t = std::chrono::duration<float>;
T* const encoder_;
Impl* const impl_;
bool finished_ = (encoder_ == nullptr);
int pts_ = -1; // valid pts starts at 0
// Actual duration of PTS unit.
time_unit_t time_scale() const;
};
const std::optional<std::size_t> max_size_;
std::optional<std::chrono::duration<float>> max_duration_;
// max_duration_ may be readjusted in case a queue
// finishes early for any reason. max_duration_config_ is
// the original, unmodified value.
const decltype(max_duration_) max_duration_config_ = max_duration_;
std::unique_ptr<MediaContainer> container_;
std::unique_ptr<AudioEncoder> audio_encoder_;
std::unique_ptr<VideoEncoder> video_encoder_;
Queue<AudioEncoder> audio_queue_ {audio_encoder_, *this};
Queue<VideoEncoder> video_queue_ {video_encoder_, *this};
// This class becomes invalid if:
//
// 1) an exception occurred
// 2) the object has begun destructing
std::atomic<bool> valid_ = true;
// Average number of frames actually encoded per second.
std::atomic<float> video_frame_rate_avg_ = 0.f;
Impl(Config config);
~Impl();
// Returns valid PTS if enough time has passed.
std::optional<int> advance_video_pts();
// Use before accessing audio_queue_ or video_queue_.
auto queue_guard() { return std::lock_guard(queue_mutex_); }
// Use to notify worker thread if queues were modified.
void wake_up_worker() { queue_cond_.notify_one(); }
private:
enum class QueueState
{
kEmpty, // all queues are empty
kFlushed, // a queue was flushed but more data may be waiting
kFinished, // all queues are finished -- no more data may be queued
};
const tic_t epoch_;
VideoEncoder::FrameCount video_frame_count_reference_ = {};
std::thread thread_;
mutable std::recursive_mutex queue_mutex_; // guards audio and video queues
std::condition_variable_any queue_cond_;
std::unique_ptr<AudioEncoder> make_audio_encoder(const Config cfg) const;
std::unique_ptr<VideoEncoder> make_video_encoder(const Config cfg) const;
QueueState encode_queues();
void update_video_frame_rate_avg();
void worker();
void container_dtor_handler(const MediaContainer& container) const;
// TODO: remove once hwr2 twodee is finished
VideoFrame::instance_t convert_indexed_video_frame(const IndexedVideoFrame& indexed);
};
template <>
inline AVRecorder::Impl::Queue<AudioEncoder>::time_unit_t AVRecorder::Impl::Queue<AudioEncoder>::time_scale() const
{
return time_unit_t(1.f / encoder_->sample_rate());
}
template <>
inline AVRecorder::Impl::Queue<VideoEncoder>::time_unit_t AVRecorder::Impl::Queue<VideoEncoder>::time_scale() const
{
return time_unit_t(1.f / encoder_->frame_rate());
}
extern template class AVRecorder::Impl::Queue<AudioEncoder>;
extern template class AVRecorder::Impl::Queue<VideoEncoder>;
}; // namespace srb2::media
#endif // __SRB2_MEDIA_AVRECORDER_IMPL_HPP__

View file

@ -0,0 +1,69 @@
// 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.
//-----------------------------------------------------------------------------
// TODO: remove this file once hwr2 twodee is finished
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include "../cxxutil.hpp"
#include "avrecorder_impl.hpp"
using namespace srb2::media;
using Impl = AVRecorder::Impl;
VideoFrame::instance_t Impl::convert_indexed_video_frame(const IndexedVideoFrame& indexed)
{
VideoFrame::instance_t frame = video_encoder_->new_frame(indexed.width, indexed.height, indexed.pts);
SRB2_ASSERT(frame != nullptr);
const VideoFrame::Buffer& buffer = frame->rgba_buffer();
const uint8_t* s = indexed.screen.data();
uint8_t* p = buffer.plane.data();
for (int y = 0; y < frame->height(); ++y)
{
for (int x = 0; x < frame->width(); ++x)
{
const RGBA_t& c = indexed.palette[s[x]];
reinterpret_cast<uint32_t*>(p)[x] = c.rgba;
}
s += indexed.width;
p += buffer.row_stride;
}
return frame;
}
AVRecorder::IndexedVideoFrame::instance_t AVRecorder::new_indexed_video_frame(uint32_t width, uint32_t height)
{
std::optional<int> pts = impl_->advance_video_pts();
if (!pts)
{
return nullptr;
}
return std::make_unique<IndexedVideoFrame>(width, height, *pts);
}
void AVRecorder::push_indexed_video_frame(IndexedVideoFrame::instance_t frame)
{
auto _ = impl_->queue_guard();
impl_->video_queue_.vec_.emplace_back(std::move(frame));
impl_->wake_up_worker();
}

View file

@ -0,0 +1,169 @@
// 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 <chrono>
#include <cstddef>
#include <mutex>
#include <utility>
#include "avrecorder_impl.hpp"
using namespace srb2::media;
using Impl = AVRecorder::Impl;
template <typename T>
bool Impl::Queue<T>::advance(int new_pts, int duration)
{
if (!impl_->valid_ || finished())
{
return false;
}
new_pts += duration;
// PTS must only advance.
if (new_pts <= pts())
{
return false;
}
auto finish = [this]
{
finished_ = true;
const auto t = impl_->container_->duration();
// Tracks are ultimately cut to the shortest among
// them, therefore it would be pointless for another
// queue to continue beyond this point.
//
// This is relevant if finishing due to size
// constraint; in that case, another queue might be
// far behind this one in terms of size and would
// continue in vain.
if (!impl_->max_duration_ || t < impl_->max_duration_)
{
impl_->max_duration_ = t;
}
impl_->wake_up_worker();
};
if (impl_->max_duration_)
{
const int final_pts = *impl_->max_duration_ / time_scale();
if (new_pts > final_pts)
{
return finish(), false;
}
}
if (impl_->max_size_)
{
constexpr float kError = 0.99f; // 1% muxing overhead
const MediaEncoder::BitRate est = encoder_->estimated_bit_rate();
const float br = est.bits / 8.f;
// count size of already queued frames too
const float t = ((duration + queued_frames_) * time_scale()) / est.period;
if ((impl_->container_->size() + (t * br)) > (*impl_->max_size_ * kError))
{
return finish(), false;
}
}
pts_ = new_pts;
queued_frames_ += duration;
return true;
}
Impl::QueueState Impl::encode_queues()
{
bool remain = false;
bool flushed = false;
auto check = [&, this](auto& q, auto encode)
{
std::unique_lock lock(queue_mutex_);
if (!q.finished())
{
remain = true;
}
if (!q.vec_.empty())
{
const std::size_t n = q.queued_frames_;
auto copy = std::move(q.vec_);
lock.unlock();
encode(std::move(copy));
lock.lock();
q.queued_frames_ -= n;
flushed = true;
}
};
auto encode_audio = [this](auto copy) { audio_encoder_->encode(copy); };
auto encode_video = [this](auto copy)
{
for (auto& p : copy)
{
auto frame = convert_indexed_video_frame(*p);
video_encoder_->encode(std::move(frame));
}
update_video_frame_rate_avg();
};
check(audio_queue_, encode_audio);
check(video_queue_, encode_video);
if (flushed)
{
return QueueState::kFlushed;
}
else if (remain)
{
return QueueState::kEmpty;
}
else
{
return QueueState::kFinished;
}
}
void Impl::update_video_frame_rate_avg()
{
constexpr auto period = std::chrono::duration<float>(1.f);
auto& ref = video_frame_count_reference_;
const auto count = video_encoder_->frame_count();
const auto t = (count.duration - ref.duration);
if (t >= period)
{
video_frame_rate_avg_ = (count.frames - ref.frames) * (period / t);
ref = count;
}
}
template class Impl::Queue<AudioEncoder>;
template class Impl::Queue<VideoEncoder>;

34
src/media/cfile.cpp Normal file
View file

@ -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 <cerrno>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <fmt/format.h>
#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_);
}

36
src/media/cfile.hpp Normal file
View file

@ -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 <cstdio>
#include <string>
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__

53
src/media/container.hpp Normal file
View file

@ -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 <chrono>
#include <functional>
#include <memory>
#include <string>
#include "audio_encoder.hpp"
#include "video_encoder.hpp"
namespace srb2::media
{
class MediaContainer
{
public:
using dtor_cb_t = std::function<void(const MediaContainer&)>;
using time_unit_t = std::chrono::duration<float>;
struct Config
{
std::string file_name;
dtor_cb_t destructor_callback;
};
virtual ~MediaContainer() = default;
virtual std::unique_ptr<AudioEncoder> make_audio_encoder(AudioEncoder::Config config) = 0;
virtual std::unique_ptr<VideoEncoder> 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__

51
src/media/encoder.hpp Normal file
View file

@ -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 <chrono>
#include <cstddef>
#include <tcb/span.hpp>
namespace srb2::media
{
class MediaEncoder
{
public:
using time_unit_t = std::chrono::duration<float>;
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<const std::byte>;
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__

117
src/media/options.cpp Normal file
View file

@ -0,0 +1,117 @@
// 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 <cstddef>
#include <cstdint>
#include <type_traits>
#include <fmt/format.h>
#include "../cxxutil.hpp"
#include "../m_fixed.h"
#include "options.hpp"
using namespace srb2::media;
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());
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<int>(const char* option) const
{
return cvar(option).value;
}
template <>
float Options::get<float>(const char* option) const
{
return FixedToFloat(cvar(option).value);
}
template <typename T>
consvar_t Options::values(const char* default_value, const Range<T> range, std::map<std::string_view, T> list)
{
constexpr bool is_float = std::is_floating_point_v<T>;
const std::size_t min_max_size = (range.min || range.max) ? 2 : 0;
auto* arr = new CV_PossibleValue_t[list.size() + min_max_size + 1];
auto cast = [is_float](T n)
{
if constexpr (is_float)
{
return FloatToFixed(n);
}
else
{
return n;
}
};
if (min_max_size)
{
// Order is very important, MIN then MAX.
arr[0] = {range.min ? cast(*range.min) : INT32_MIN, "MIN"};
arr[1] = {range.max ? cast(*range.max) : INT32_MAX, "MAX"};
}
{
std::size_t i = min_max_size;
for (const auto& [k, v] : list)
{
arr[i].value = cast(v);
arr[i].strvalue = k.data();
i++;
}
arr[i].value = 0;
arr[i].strvalue = nullptr;
}
int32_t flags = CV_SAVE;
if constexpr (is_float)
{
flags |= CV_FLOAT;
}
return CVAR_INIT(nullptr, default_value, flags, arr, nullptr);
}
void Options::register_all()
{
for (auto cvar : cvars_)
{
CV_RegisterVar(cvar);
}
cvars_ = {};
}
// clang-format off
template consvar_t Options::values(const char* default_value, const Range<int> range, std::map<std::string_view, int> list);
template consvar_t Options::values(const char* default_value, const Range<float> range, std::map<std::string_view, float> list);
// clang-format on

58
src/media/options.hpp Normal file
View file

@ -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_OPTIONS_HPP__
#define __SRB2_MEDIA_OPTIONS_HPP__
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "../command.h"
namespace srb2::media
{
class Options
{
public:
using map_t = std::unordered_map<std::string, consvar_t>;
template <typename T>
struct Range
{
std::optional<T> min, max;
};
// Registers all options as cvars.
static void register_all();
Options(const char* prefix, map_t map);
template <typename T>
T get(const char* option) const;
template <typename T>
static consvar_t values(const char* default_value, const Range<T> range, std::map<std::string_view, T> list = {});
private:
static std::vector<consvar_t*> cvars_;
const char* prefix_;
map_t map_;
const consvar_t& cvar(const char* option) const;
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_OPTIONS_HPP__

View file

@ -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.
//-----------------------------------------------------------------------------
#include <cstdint>
#include <vpx/vpx_encoder.h>
#include "options.hpp"
#include "vorbis.hpp"
#include "vp8.hpp"
using namespace srb2::media;
// NOTE: Options::cvars_ MUST be initialized before any
// Options instances construct. For static objects, they have
// to be defined in the same translation unit as
// Options::cvars_ to guarantee initialization order.
std::vector<consvar_t*> Options::cvars_;
// clang-format off
const Options VorbisEncoder::options_("vorbis", {
{"quality", Options::values<float>("0", {-0.1f, 1.f})},
{"max_bitrate", Options::values<int>("-1", {-1})},
{"nominal_bitrate", Options::values<int>("-1", {-1})},
{"min_bitrate", Options::values<int>("-1", {-1})},
});
const Options VP8Encoder::options_("vp8", {
{"quality_mode", Options::values<int>("q", {}, {
{"vbr", VPX_VBR},
{"cbr", VPX_CBR},
{"cq", VPX_CQ},
{"q", VPX_Q},
})},
{"target_bitrate", Options::values<int>("800", {1})},
{"min_q", Options::values<int>("4", {4, 63})},
{"max_q", Options::values<int>("55", {4, 63})},
{"kf_min", Options::values<int>("0", {0})},
{"kf_max", Options::values<int>("auto", {0}, {
{"auto", static_cast<int>(KeyFrameOption::kAuto)},
})},
{"cpu_used", Options::values<int>("0", {-16, 16})},
{"cq_level", Options::values<int>("10", {0, 63})},
{"deadline", Options::values<int>("10", {1}, {
{"infinite", static_cast<int>(DeadlineOption::kInfinite)},
})},
{"sharpness", Options::values<int>("7", {0, 7})},
{"token_parts", Options::values<int>("0", {0, 3})},
{"threads", Options::values<int>("1", {1})},
});
// clang-format on

View file

@ -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__

63
src/media/video_frame.hpp Normal file
View file

@ -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 <cstddef>
#include <cstdint>
#include <memory>
#include <tcb/span.hpp>
namespace srb2::media
{
class VideoFrame
{
public:
using instance_t = std::unique_ptr<VideoFrame>;
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<uint8_t> 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__

142
src/media/vorbis.cpp Normal file
View file

@ -0,0 +1,142 @@
// 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 <chrono>
#include <cstddef>
#include <stdexcept>
#include <fmt/format.h>
#include <vorbis/vorbisenc.h>
#include "../cxxutil.hpp"
#include "vorbis.hpp"
#include "vorbis_error.hpp"
using namespace srb2::media;
namespace
{
void runtime_assert(VorbisError error, const char *what)
{
if (error != 0)
{
throw std::runtime_error(fmt::format("{}: {}", what, error));
}
}
}; // namespace
VorbisEncoder::VorbisEncoder(Config cfg)
{
const long max_bitrate = options_.get<int>("max_bitrate");
const long nominal_bitrate = options_.get<int>("nominal_bitrate");
const long min_bitrate = options_.get<int>("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<float>("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));
}
}
runtime_assert(vorbis_analysis_init(&vd_, &vi_), "vorbis_analysis_init");
runtime_assert(vorbis_block_init(&vd_, &vb_), "vorbis_block_init");
}
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
runtime_assert(vorbis_analysis_wrote(&vd_, n), "vorbis_analysis_wrote");
while (vorbis_analysis_blockout(&vd_, &vb_) > 0)
{
runtime_assert(vorbis_analysis(&vb_, nullptr), "vorbis_analysis");
runtime_assert(vorbis_bitrate_addblock(&vb_), "vorbis_bitrate_addblock");
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<T> p(reinterpret_cast<T*>(op->packet), static_cast<std::size_t>(op->bytes));
write_frame(p, std::chrono::duration<float>(vorbis_granule_time(&vd_, op->granulepos)), true);
}

54
src/media/vorbis.hpp Normal file
View file

@ -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 <array>
#include <vorbis/codec.h>
#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<ogg_packet, 3>;
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__

View file

@ -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 <string>
#include <fmt/format.h>
#include <vorbis/codec.h>
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<VorbisError> : formatter<std::string>
{
template <typename FormatContext>
auto format(const VorbisError& error, FormatContext& ctx) const
{
return formatter<std::string>::format(error.name(), ctx);
}
};
#endif // __SRB2_MEDIA_VORBIS_ERROR_HPP__

241
src/media/vp8.cpp Normal file
View file

@ -0,0 +1,241 @@
// 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 <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <fmt/format.h>
#include <tcb/span.hpp>
#include "../cxxutil.hpp"
#include "vp8.hpp"
#include "vpx_error.hpp"
#include "yuv420p.hpp"
using namespace srb2::media;
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<int>("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<vpx_rc_mode>(options_.get<int>("quality_mode"));
cfg.kf_mode = VPX_KF_AUTO;
cfg.rc_target_bitrate = options_.get<int>("target_bitrate");
cfg.rc_min_quantizer = options_.get<int>("min_q");
cfg.rc_max_quantizer = options_.get<int>("max_q");
// Keyframe spacing, in number of frames.
// kf_max_dist should be low enough to allow scrubbing.
int kf_max = options_.get<int>("kf_max");
if (kf_max == static_cast<int>(KeyFrameOption::kAuto))
{
// Automatically pick a good rate
kf_max = (user.frame_rate / 2); // every .5s
}
cfg.kf_min_dist = options_.get<int>("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<int>(VP8E_SET_CPUUSED, "cpu_used");
control<int>(VP8E_SET_CQ_LEVEL, "cq_level");
control<int>(VP8E_SET_SHARPNESS, "sharpness");
control<int>(VP8E_SET_TOKEN_PARTITIONS, "token_parts");
auto plane = [this](int k, int ycs = 0)
{
using T = uint8_t;
auto view = tcb::span<T>(reinterpret_cast<T*>(img_->planes[k]), img_->stride[k] * (img_->h >> ycs));
return VideoFrame::Buffer {view, static_cast<std::size_t>(img_->stride[k])};
};
frame_ = std::make_unique<YUV420pFrame>(
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)
{
if (vpx_img_alloc(&img_, VPX_IMG_FMT_I420, width, height, YUV420pFrame::kAlignment) == nullptr)
{
throw std::runtime_error("vpx_img_alloc");
}
}
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<T*>(frame.get()) != nullptr);
frame_ = std::unique_ptr<T>(static_cast<T*>(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<float>(frame_rate());
using T = const std::byte;
tcb::span<T> p(reinterpret_cast<T*>(frame.buf), frame.sz);
write_frame(p, std::chrono::duration<float>(ts), (frame.flags & VPX_FRAME_IS_KEY));
}
return output;
}
template <typename T>
void VP8Encoder::control(vp8e_enc_control_id id, const char* option)
{
auto value = options_.get<T>(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<float>(duration_ / static_cast<float>(frame_rate()))};
}

112
src/media/vp8.hpp Normal file
View file

@ -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_VP8_HPP__
#define __SRB2_MEDIA_VP8_HPP__
#include <mutex>
#include <vpx/vp8cx.h>
#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_;
};
enum class KeyFrameOption : int
{
kAuto = -1,
};
enum class DeadlineOption : int
{
kInfinite = 0,
};
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<int>("threads");
const int deadline_ = options_.get<int>("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<YUV420pFrame> frame_;
bool process();
template <typename T> // T = option type
void control(vp8e_enc_control_id id, const char* option);
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_VP8_HPP__

45
src/media/vpx_error.hpp Normal file
View file

@ -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 <string>
#include <fmt/format.h>
#include <vpx/vpx_codec.h>
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<VpxError> : formatter<std::string>
{
template <typename FormatContext>
auto format(const VpxError& error, FormatContext& ctx) const
{
return formatter<std::string>::format(error.description(), ctx);
}
};
#endif // __SRB2_MEDIA_VPX_ERROR_HPP__

26
src/media/webm.hpp Normal file
View file

@ -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 <chrono>
#include <cstdint>
#include <ratio>
namespace srb2::media::webm
{
using track = uint64_t;
using timestamp = uint64_t;
using duration = std::chrono::duration<timestamp, std::nano>;
}; // namespace srb2::media::webm
#endif // __SRB2_MEDIA_WEBM_HPP__

View file

@ -0,0 +1,241 @@
// 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 <cstdint>
#include <memory>
#include <stdexcept>
#include <fmt/format.h>
#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)
{
if (!segment_.Init(&writer_))
{
throw std::runtime_error("mkvmuxer::Segment::Init");
}
}
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<AudioEncoder> WebmContainer::make_audio_encoder(AudioEncoder::Config cfg)
{
const uint64_t tid = segment_.AddAudioTrack(cfg.sample_rate, cfg.channels, 0);
return std::make_unique<WebmVorbisEncoder>(*this, tid, cfg);
}
std::unique_ptr<VideoEncoder> WebmContainer::make_video_encoder(VideoEncoder::Config cfg)
{
const uint64_t tid = segment_.AddVideoTrack(cfg.width, cfg.height, 0);
return std::make_unique<WebmVP8Encoder>(*this, tid, cfg);
}
time_unit_t WebmContainer::duration() const
{
if (finalized_)
{
const auto& si = *segment_.segment_info();
return webm::duration(static_cast<uint64_t>(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<const std::byte> buffer,
webm::track trackid,
webm::timestamp timestamp,
bool is_key_frame
)
{
if (!segment_.AddFrame(
reinterpret_cast<const uint8_t*>(buffer.data()),
buffer.size_bytes(),
trackid,
timestamp,
is_key_frame
))
{
throw std::runtime_error(fmt::format(
"mkvmuxer::Segment::AddFrame, size={}, track={}, ts={}, key={}",
buffer.size_bytes(),
trackid,
timestamp,
is_key_frame
));
}
queue_[trackid].data_size += buffer.size_bytes();
}
void WebmContainer::queue_frame(
tcb::span<const std::byte> 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;
}

View file

@ -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 <cstddef>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <mkvmuxer/mkvmuxer.h>
#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<AudioEncoder> make_audio_encoder(AudioEncoder::Config config) override final;
virtual std::unique_ptr<VideoEncoder> 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 <typename T = mkvmuxer::Track>
T* get_track(webm::track trackid) const
{
return reinterpret_cast<T*>(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<const std::byte> 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<std::byte> buffer;
webm::timestamp timestamp;
bool is_key_frame;
Frame(tcb::span<const std::byte> buffer_, webm::timestamp timestamp_, bool is_key_frame_) :
buffer(buffer_.begin(), buffer_.end()), timestamp(timestamp_), is_key_frame(is_key_frame_)
{
}
};
std::vector<Frame> 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<webm::track, FrameQueue> 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<const std::byte> 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__

View file

@ -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 <mkvmuxer/mkvmuxer.h>
#include "encoder.hpp"
#include "webm_container.hpp"
namespace srb2::media
{
template <typename T = mkvmuxer::Track>
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<T>(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<webm::duration>(ts);
container_.queue_frame(p, trackid_, ts_nano.count(), is_key_frame);
}
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_ENCODER_HPP__

65
src/media/webm_vorbis.hpp Normal file
View file

@ -0,0 +1,65 @@
// 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 <chrono>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <vector>
#include <fmt/format.h>
#include "../cxxutil.hpp"
#include "vorbis.hpp"
#include "webm_encoder.hpp"
namespace srb2::media
{
class WebmVorbisEncoder : public WebmEncoder<mkvmuxer::AudioTrack>, 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();
if (!track()->SetCodecPrivate(reinterpret_cast<const uint8_t*>(p.data()), p.size()))
{
throw std::runtime_error(fmt::format("mkvmuxer::AudioTrack::SetCodecPrivate, size={}", p.size()));
}
}
virtual BitRate estimated_bit_rate() const override final
{
auto _ = container_.queue_guard();
const std::chrono::duration<float> t = duration();
if (t <= t.zero())
{
return {};
}
using namespace std::chrono_literals;
return {static_cast<std::size_t>((size() * 8) / t.count()), 1s};
}
private:
std::vector<std::byte> make_vorbis_private_data();
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_VORBIS_HPP__

View file

@ -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 <algorithm>
#include <cstddef>
#include <iterator>
#include <vector>
#include <tcb/span.hpp>
#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<std::byte>& 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<std::byte> WebmVorbisEncoder::make_vorbis_private_data()
{
const headers_t packets = generate_headers();
std::vector<std::byte> 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<const std::byte> p(reinterpret_cast<const std::byte*>(op.packet), op.bytes);
std::copy(p.begin(), p.end(), std::back_inserter(v));
}
return v;
}

44
src/media/webm_vp8.hpp Normal file
View file

@ -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<mkvmuxer::VideoTrack>, 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<float>(1.f / frame_rate())};
}
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_VP8_HPP__

32
src/media/webm_writer.hpp Normal file
View file

@ -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 <cstdio>
#include <string>
#include <mkvmuxer/mkvwriter.h>
#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<std::FILE*>(*this)) {}
~WebmWriter() { MkvWriter::Close(); }
};
}; // namespace srb2::media
#endif // __SRB2_MEDIA_WEBM_WRITER_HPP__

125
src/media/yuv420p.cpp Normal file
View file

@ -0,0 +1,125 @@
// 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 <algorithm>
#include <cstdint>
#include <memory>
#include <libyuv/convert.h>
#include <libyuv/scale_argb.h>
#include <tcb/span.hpp>
#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();
p = std::align(kAlignment, 1, p, n);
SRB2_ASSERT(p != nullptr);
plane = tcb::span<uint8_t>(reinterpret_cast<uint8_t*>(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<float>(height());
const float rs = vw / static_cast<float>(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;
}

74
src/media/yuv420p.hpp Normal file
View file

@ -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 <cstdint>
#include <vector>
#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<uint8_t> 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__

View file

@ -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;
@ -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...
@ -174,7 +184,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));
@ -256,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++;
@ -270,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.
@ -318,11 +357,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 +456,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);
@ -461,8 +503,8 @@ boolean M_ChallengesInputs(INT32 ch)
}
if (!(challengesmenu.extradata[
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
+ challengesmenu.row]
& CHE_CONNECTEDUP))
+ challengesmenu.row
].flags & CHE_CONNECTEDUP))
{
break;
}
@ -475,8 +517,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 +558,8 @@ boolean M_ChallengesInputs(INT32 ch)
if (!(challengesmenu.extradata[
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
+ challengesmenu.row]
& CHE_CONNECTEDLEFT))
+ challengesmenu.row
].flags & CHE_CONNECTEDLEFT))
{
break;
}
@ -531,8 +573,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 +612,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)
{
@ -586,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;
}

View file

@ -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();
}
@ -797,7 +798,7 @@ void P_CheckPointLimit(void)
if (!K_CanChangeRules(true))
return;
if (!cv_pointlimit.value)
if (!g_pointlimit)
return;
if (!(gametyperules & GTR_POINTLIMIT))
@ -810,7 +811,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 +824,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 ;;
{

View file

@ -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);
@ -4984,6 +4988,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 +5160,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;

View file

@ -21,6 +21,7 @@
#include "../audio/sound_effect_player.hpp"
#include "../cxxutil.hpp"
#include "../io/streams.hpp"
#include "../m_avrecorder.hpp"
#include "../doomdef.h"
#include "../i_sound.h"
@ -57,6 +58,8 @@ static shared_ptr<Gain<2>> gain_music;
static vector<shared_ptr<SoundEffectPlayer>> sound_effect_channels;
static shared_ptr<srb2::media::AVRecorder> av_recorder;
static void (*music_fade_callback)();
void* I_GetSfx(sfxinfo_t* sfx)
@ -135,6 +138,9 @@ void audio_callback(void* userdata, Uint8* buffer, int len)
std::clamp(float_buffer[i].amplitudes[1], -1.f, 1.f),
};
}
if (av_recorder)
av_recorder->push_audio_samples(tcb::span {float_buffer, float_len});
}
catch (...)
{
@ -749,3 +755,11 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
else
return false;
}
void I_UpdateAudioRecorder(void)
{
// must be locked since av_recorder is used by audio_callback
SdlAudioLockHandle _;
av_recorder = g_av_recorder;
}

View file

@ -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

View file

@ -1168,6 +1168,7 @@ typedef enum
sfx_typri1,
sfx_typri2,
sfx_eggspr,
sfx_achiev,
// SRB2Kart - Drop target sounds
sfx_kdtrg1,

View file

@ -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);

View file

@ -15,6 +15,9 @@ include("cpm-sdl2.cmake")
include("cpm-png.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")
@ -23,6 +26,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)

11
thirdparty/cpm-libvorbis.cmake vendored Normal file
View file

@ -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()

37
thirdparty/cpm-libvpx.cmake vendored Normal file
View file

@ -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()

31
thirdparty/cpm-libwebm.cmake vendored Normal file
View file

@ -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()

77
thirdparty/cpm-libyuv.cmake vendored Normal file
View file

@ -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()

13
thirdparty/cpm-ogg.cmake vendored Normal file
View file

@ -0,0 +1,13 @@
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
)
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()