diff --git a/CMakeLists.txt b/CMakeLists.txt index 455c4b0b4..cdbb760c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ cmake_dependent_option( OFF "NOT SRB2_CONFIG_SYSTEM_LIBRARIES" OFF ) +option(SRB2_CONFIG_ENABLE_WEBM_MOVIES "Enable WebM recording support" ON) option(SRB2_CONFIG_HWRENDER "Enable hardware render (OpenGL) support" ON) option(SRB2_CONFIG_STATIC_OPENGL "Enable static linking GL (do not do this)" OFF) option(SRB2_CONFIG_ERRORMODE "Compile C code with warnings treated as errors." OFF) @@ -132,9 +133,11 @@ 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) + if (SRB2_CONFIG_ENABLE_WEBM_MOVIES) + find_package(VPX REQUIRED) + find_package(Vorbis REQUIRED) + find_package(VorbisEnc REQUIRED) + endif() endif() if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 898fcc789..4e68db60b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,13 +32,13 @@ 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 m_easing.c m_fixed.c - m_misc.c + m_memcpy.c + m_misc.cpp m_perfstats.c m_random.c m_queue.c @@ -48,6 +48,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 p_floor.c p_inter.c p_lights.c + p_loop.c p_map.c p_maputl.c p_mobj.c @@ -135,6 +136,10 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 k_roulette.c ) +if(SRB2_CONFIG_ENABLE_WEBM_MOVIES) + target_sources(SRB2SDL2 PRIVATE m_avrecorder.cpp) +endif() + # This updates the modification time for comptime.c at the # end of building so when the build system is ran next time, # that file gets flagged. comptime.c will always be rebuilt. @@ -225,14 +230,18 @@ target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_DISCORDRPC -DUSE_STUN) target_sources(SRB2SDL2 PRIVATE discord.c stun.c) target_link_libraries(SRB2SDL2 PRIVATE tcbrindle::span) +target_link_libraries(SRB2SDL2 PRIVATE stb_rect_pack) target_link_libraries(SRB2SDL2 PRIVATE stb_vorbis) 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) +if(SRB2_CONFIG_ENABLE_WEBM_MOVIES) + 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_compile_definitions(SRB2SDL2 PRIVATE -DSRB2_CONFIG_ENABLE_WEBM_MOVIES) +endif() target_link_libraries(SRB2SDL2 PRIVATE acsvm) @@ -550,7 +559,9 @@ if(SRB2_CONFIG_ENABLE_TESTS) add_subdirectory(tests) endif() add_subdirectory(menus) -add_subdirectory(media) +if(SRB2_CONFIG_ENABLE_WEBM_MOVIES) + add_subdirectory(media) +endif() # strip debug symbols into separate file when using gcc. # to be consistent with Makefile, don't generate for OS X. diff --git a/src/acs/call-funcs.cpp b/src/acs/call-funcs.cpp index dc413ef55..b6e1ce3a8 100644 --- a/src/acs/call-funcs.cpp +++ b/src/acs/call-funcs.cpp @@ -559,10 +559,7 @@ bool CallFunc_EndPrint(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Wo (void)argC; if (ACS_ActivatorIsLocal(thread) == true) - { - HU_SetCEchoDuration(5); - HU_DoCEcho(thread->printBuf.data()); - } + HU_DoTitlecardCEcho(thread->printBuf.data()); thread->printBuf.drop(); return false; @@ -931,13 +928,11 @@ bool CallFunc_EndPrintBold(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM (void)argV; (void)argC; - HU_SetCEchoDuration(5); - HU_DoCEcho(thread->printBuf.data()); + HU_DoTitlecardCEcho(thread->printBuf.data()); thread->printBuf.drop(); return false; } - /*-------------------------------------------------- bool CallFunc_PlayerTeam(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC) diff --git a/src/acs/environment.cpp b/src/acs/environment.cpp index a2eb093d4..9c5c5ca61 100644 --- a/src/acs/environment.cpp +++ b/src/acs/environment.cpp @@ -103,7 +103,7 @@ Environment::Environment() addCodeDataACS0(120, {"", 0, addCallFunc(CallFunc_PlayerRings)}); addCodeDataACS0(122, {"", 0, addCallFunc(CallFunc_PlayerScore)}); - + // 136 to 137: Implemented by ACSVM // 157: Implemented by ACSVM diff --git a/src/cxxutil.hpp b/src/cxxutil.hpp index 06f6f1adc..56b85c79b 100644 --- a/src/cxxutil.hpp +++ b/src/cxxutil.hpp @@ -162,6 +162,17 @@ struct Overload : Ts... { template Overload(Ts...) -> Overload; +inline void hash_combine(std::size_t& seed) +{} + +template +inline void hash_combine(std::size_t& seed, const T& v, Rest... rest) +{ + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + hash_combine(seed, std::forward(rest)...); +} + } // namespace srb2 #endif // __SRB2_CXXUTIL_HPP__ diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 860f14b04..0a6757f35 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -1879,7 +1879,7 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic return false; } case CL_LOADFILES: - if (CL_LoadServerFiles()) + if (CL_LoadServerFiles()) cl_mode = CL_SETUPFILES; break; @@ -2021,8 +2021,10 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic #endif } I_UpdateNoVsync(); // page flip or blit buffer - if (moviemode) - M_SaveFrame(); +#ifdef HWRENDER + if (moviemode && rendermode == render_opengl) + M_LegacySaveFrame(); +#endif S_UpdateSounds(); S_UpdateClosedCaptions(); } @@ -2540,7 +2542,7 @@ void CL_ClearPlayer(INT32 playernum) if (gamestate == GS_LEVEL) { if (players[playernum].follower) - { + { K_RemoveFollower(&players[playernum]); } diff --git a/src/d_main.c b/src/d_main.c index 2f75bb5a9..f41d70a31 100644 --- a/src/d_main.c +++ b/src/d_main.c @@ -343,7 +343,7 @@ static void D_Display(void) F_WipeStartScreen(); F_WipeColorFill(31); F_WipeEndScreen(); - F_RunWipe(wipetypepre, gamestate != GS_MENU, "FADEMAP0", false, false); + F_RunWipe(wipedefindex, wipetypepre, gamestate != GS_MENU, "FADEMAP0", false, false); } if (gamestate != GS_LEVEL && rendermode != render_none) @@ -356,7 +356,7 @@ static void D_Display(void) } else //dedicated servers { - F_RunWipe(wipedefs[wipedefindex], gamestate != GS_MENU, "FADEMAP0", false, false); + F_RunWipe(wipedefindex, wipedefs[wipedefindex], gamestate != GS_MENU, "FADEMAP0", false, false); wipegamestate = gamestate; } @@ -633,7 +633,7 @@ static void D_Display(void) { F_WipeEndScreen(); - F_RunWipe(wipedefs[wipedefindex], gamestate != GS_MENU && gamestate != GS_TITLESCREEN, "FADEMAP0", true, false); + F_RunWipe(wipedefindex, wipedefs[wipedefindex], gamestate != GS_MENU && gamestate != GS_TITLESCREEN, "FADEMAP0", true, false); } // reset counters so timedemo doesn't count the wipe duration @@ -690,6 +690,9 @@ static void D_Display(void) ps_swaptime = I_GetPreciseTime(); I_FinishUpdate(); // page flip or blit buffer ps_swaptime = I_GetPreciseTime() - ps_swaptime; + + // We should never do the HWR2 skip 3d drawing hack for more than 1 full draw. + g_wipeskiprender = false; } } @@ -800,6 +803,9 @@ void D_SRB2Loop(void) HW3S_BeginFrameUpdate(); #endif + I_NewTwodeeFrame(); + I_NewImguiFrame(); + if (realtics > 0 || singletics) { // don't skip more than 10 frames at a time @@ -865,11 +871,13 @@ void D_SRB2Loop(void) D_Display(); } +#ifdef HWRENDER // Only take screenshots after drawing. - if (moviemode) - M_SaveFrame(); - if (takescreenshot) - M_DoScreenShot(); + if (moviemode && rendermode == render_opengl) + M_LegacySaveFrame(); + if (rendermode == render_opengl && takescreenshot) + M_DoLegacyGLScreenShot(); +#endif // consoleplayer -> displayplayers (hear sounds from viewpoint) S_UpdateSounds(); // move positional sounds @@ -1489,6 +1497,9 @@ void D_SRB2Main(void) CONS_Printf("I_StartupGraphics()...\n"); I_StartupGraphics(); + I_NewTwodeeFrame(); + I_NewImguiFrame(); + #ifdef HWRENDER // Lactozilla: Add every hardware mode CVAR and CCMD. // Has to be done before the configuration file loads, diff --git a/src/d_netcmd.c b/src/d_netcmd.c index e0491a74c..4b02e962f 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -62,7 +62,10 @@ #include "deh_tables.h" #include "m_perfstats.h" #include "k_specialstage.h" + +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #include "m_avrecorder.h" +#endif #ifdef HAVE_DISCORDRPC #include "discord.h" @@ -545,8 +548,6 @@ static CV_PossibleValue_t perfstats_cons_t[] = { }; consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", 0, perfstats_cons_t, NULL); -consvar_t cv_director = CVAR_INIT ("director", "Off", 0, CV_OnOff, NULL); - consvar_t cv_schedule = CVAR_INIT ("schedule", "On", CV_NETVAR|CV_CALL, CV_OnOff, Schedule_OnChange); consvar_t cv_automate = CVAR_INIT ("automate", "On", CV_NETVAR, CV_OnOff, NULL); @@ -904,7 +905,11 @@ void D_RegisterClientCommands(void) CV_RegisterVar(&cv_moviemode); CV_RegisterVar(&cv_movie_option); CV_RegisterVar(&cv_movie_folder); + +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES M_AVRecorder_AddCommands(); +#endif + // PNG variables CV_RegisterVar(&cv_zlib_level); CV_RegisterVar(&cv_zlib_memory); @@ -1053,8 +1058,6 @@ void D_RegisterClientCommands(void) CV_RegisterVar(&cv_scr_width); CV_RegisterVar(&cv_scr_height); - CV_RegisterVar(&cv_director); - CV_RegisterVar(&cv_soundtest); CV_RegisterVar(&cv_invincmusicfade); @@ -4096,7 +4099,7 @@ void Schedule_Insert(scheduleTask_t *addTask) { schedule_size *= 2; } - + schedule = Z_ReallocAlign( (void*) schedule, sizeof(scheduleTask_t*) * schedule_size, diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 61171d393..a126de023 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -128,8 +128,6 @@ extern consvar_t cv_sleep; extern consvar_t cv_perfstats; -extern consvar_t cv_director; - extern consvar_t cv_schedule; extern consvar_t cv_livestudioaudience; diff --git a/src/d_player.h b/src/d_player.h index 12cab4a9a..298c65485 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -374,6 +374,16 @@ struct itemroulette_t boolean eggman; }; +// player_t struct for loop state +typedef struct { + fixed_t radius; + fixed_t revolution, min_revolution, max_revolution; + angle_t yaw; + vector3_t origin; + vector2_t shift; + boolean flip; +} sonicloopvars_t; + // ======================================================================== // PLAYER STRUCTURE // ======================================================================== @@ -668,6 +678,8 @@ struct player_t #ifdef HWRENDER fixed_t fovadd; // adjust FOV for hw rendering #endif + + sonicloopvars_t loop; }; #ifdef __cplusplus diff --git a/src/deh_tables.c b/src/deh_tables.c index 07c69f687..ab5e7959c 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -5664,6 +5664,9 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_SPECIAL_UFO", "MT_SPECIAL_UFO_PIECE", + + "MT_LOOPENDPOINT", + "MT_LOOPCENTERPOINT", }; const char *const MOBJFLAG_LIST[] = { diff --git a/src/f_finale.c b/src/f_finale.c index af0ab00dc..acd923765 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -313,7 +313,7 @@ void F_StartIntro(void) F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_intro_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_intro_toblack, wipedefs[wipe_intro_toblack], false, "FADEMAP0", false, false); } S_StopMusic(); @@ -409,7 +409,7 @@ void F_IntroTicker(void) F_WipeStartScreen(); F_WipeColorFill(31); F_WipeEndScreen(); - F_RunWipe(99, true, "FADEMAP0", false, false); + F_RunWipe(wipe_intro_toblack, 99, true, "FADEMAP0", false, false); } // Stay on black for a bit. =) @@ -437,8 +437,10 @@ void F_IntroTicker(void) #endif I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001 - if (moviemode) // make sure we save frames for the white hold too - M_SaveFrame(); +#ifdef HWRENDER + if (moviemode && rendermode == render_opengl) // make sure we save frames for the white hold too + M_LegacySaveFrame(); +#endif } } @@ -464,7 +466,7 @@ void F_IntroTicker(void) // check for skipping if (keypressed) keypressed = false; - + if (animtimer > 0) animtimer--; } @@ -2435,7 +2437,7 @@ void F_CutsceneDrawer(void) V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, cutscenes[cutnum]->scene[scenenum].fadecolor); F_WipeEndScreen(); - F_RunWipe(cutscenes[cutnum]->scene[scenenum].fadeinid, true, NULL, false, false); + F_RunWipe(wipe_intro_toblack, cutscenes[cutnum]->scene[scenenum].fadeinid, true, NULL, false, false); F_WipeStartScreen(); } @@ -2455,7 +2457,7 @@ void F_CutsceneDrawer(void) if (dofadenow && rendermode != render_none) { F_WipeEndScreen(); - F_RunWipe(cutscenes[cutnum]->scene[scenenum].fadeoutid, true, NULL, false, false); + F_RunWipe(wipe_intro_toblack, cutscenes[cutnum]->scene[scenenum].fadeoutid, true, NULL, false, false); } V_DrawString(textxpos, textypos, V_ALLOWLOWERCASE, cutscene_disptext); diff --git a/src/f_finale.h b/src/f_finale.h index ca1108214..c5972f9d1 100644 --- a/src/f_finale.h +++ b/src/f_finale.h @@ -140,6 +140,12 @@ extern UINT16 curtttics; // extern boolean WipeInAction; +extern UINT8 g_wipemode; +extern UINT8 g_wipetype; +extern UINT8 g_wipeframe; +extern boolean g_wipereverse; +extern boolean g_wipeskiprender; +extern boolean g_wipeencorewiggle; extern boolean WipeStageTitle; extern INT32 lastwipetic; @@ -150,11 +156,19 @@ extern INT32 lastwipetic; void F_WipeStartScreen(void); void F_WipeEndScreen(void); -void F_RunWipe(UINT8 wipetype, boolean drawMenu, const char *colormap, boolean reverse, boolean encorewiggle); +void F_RunWipe(UINT8 wipemode, UINT8 wipetype, boolean drawMenu, const char *colormap, boolean reverse, boolean encorewiggle); void F_WipeStageTitle(void); #define F_WipeColorFill(c) V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, c) tic_t F_GetWipeLength(UINT8 wipetype); boolean F_WipeExists(UINT8 wipetype); +/// @brief true if the wipetype is to-black +boolean F_WipeIsToBlack(UINT8 wipemode); +/// @brief true if the wipetype is to-white +boolean F_WipeIsToWhite(UINT8 wipemode); +/// @brief true if the wipetype is to-invert +boolean F_WipeIsToInvert(UINT8 wipemode); +/// @brief true if the wipetype is modulated from the previous frame +boolean F_WipeIsCrossfade(UINT8 wipemode); enum { diff --git a/src/f_wipe.c b/src/f_wipe.c index b15c4b171..227573c46 100644 --- a/src/f_wipe.c +++ b/src/f_wipe.c @@ -85,11 +85,149 @@ UINT8 wipedefs[NUMWIPEDEFS] = { 99 // wipe_cutscene_final (hardcoded) }; +static boolean g_wipedef_toblack[NUMWIPEDEFS] = { + true, // wipe_credits_intermediate (0) + + true, // wipe_level_toblack + true, // wipe_intermission_toblack + true, // wipe_voting_toblack, + true, // wipe_continuing_toblack + true, // wipe_titlescreen_toblack + true, // wipe_menu_toblack + true, // wipe_credits_toblack + true, // wipe_evaluation_toblack + true, // wipe_gameend_toblack + true, // wipe_intro_toblack (hardcoded) + true, // wipe_ending_toblack (hardcoded) + true, // wipe_cutscene_toblack (hardcoded) + + false, // wipe_encore_toinvert + false, // wipe_encore_towhite + + true, // wipe_level_final + true, // wipe_intermission_final + true, // wipe_voting_final + true, // wipe_continuing_final + true, // wipe_titlescreen_final + true, // wipe_menu_final + true, // wipe_credits_final + true, // wipe_evaluation_final + true, // wipe_gameend_final + true, // wipe_intro_final (hardcoded) + true, // wipe_ending_final (hardcoded) + true // wipe_cutscene_final (hardcoded) +}; + +static boolean g_wipedef_toinvert[NUMWIPEDEFS] = { + false, // wipe_credits_intermediate (0) + + false, // wipe_level_toblack + false, // wipe_intermission_toblack + false, // wipe_voting_toblack, + false, // wipe_continuing_toblack + false, // wipe_titlescreen_toblack + false, // wipe_menu_toblack + false, // wipe_credits_toblack + false, // wipe_evaluation_toblack + false, // wipe_gameend_toblack + false, // wipe_intro_toblack (hardcoded) + false, // wipe_ending_toblack (hardcoded) + false, // wipe_cutscene_toblack (hardcoded) + + true, // wipe_encore_toinvert + false, // wipe_encore_towhite + + false, // wipe_level_final + false, // wipe_intermission_final + false, // wipe_voting_final + false, // wipe_continuing_final + false, // wipe_titlescreen_final + false, // wipe_menu_final + false, // wipe_credits_final + false, // wipe_evaluation_final + false, // wipe_gameend_final + false, // wipe_intro_final (hardcoded) + false, // wipe_ending_final (hardcoded) + false // wipe_cutscene_final (hardcoded) +}; + +static boolean g_wipedef_towhite[NUMWIPEDEFS] = { + false, // wipe_credits_intermediate (0) + + false, // wipe_level_toblack + false, // wipe_intermission_toblack + false, // wipe_voting_toblack, + false, // wipe_continuing_toblack + false, // wipe_titlescreen_toblack + false, // wipe_menu_toblack + false, // wipe_credits_toblack + false, // wipe_evaluation_toblack + false, // wipe_gameend_toblack + false, // wipe_intro_toblack (hardcoded) + false, // wipe_ending_toblack (hardcoded) + false, // wipe_cutscene_toblack (hardcoded) + + false, // wipe_encore_toinvert + true, // wipe_encore_towhite + + false, // wipe_level_final + false, // wipe_intermission_final + false, // wipe_voting_final + false, // wipe_continuing_final + false, // wipe_titlescreen_final + false, // wipe_menu_final + false, // wipe_credits_final + false, // wipe_evaluation_final + false, // wipe_gameend_final + false, // wipe_intro_final (hardcoded) + false, // wipe_ending_final (hardcoded) + false // wipe_cutscene_final (hardcoded) +}; + +static boolean g_wipedef_crossfade[NUMWIPEDEFS] = { + false, // wipe_credits_intermediate (0) + + false, // wipe_level_toblack + false, // wipe_intermission_toblack + false, // wipe_voting_toblack, + false, // wipe_continuing_toblack + false, // wipe_titlescreen_toblack + false, // wipe_menu_toblack + false, // wipe_credits_toblack + false, // wipe_evaluation_toblack + false, // wipe_gameend_toblack + false, // wipe_intro_toblack (hardcoded) + false, // wipe_ending_toblack (hardcoded) + false, // wipe_cutscene_toblack (hardcoded) + + false, // wipe_encore_toinvert + false, // wipe_encore_towhite + + true, // wipe_level_final + true, // wipe_intermission_final + true, // wipe_voting_final + true, // wipe_continuing_final + true, // wipe_titlescreen_final + true, // wipe_menu_final + true, // wipe_credits_final + true, // wipe_evaluation_final + true, // wipe_gameend_final + true, // wipe_intro_final (hardcoded) + true, // wipe_ending_final (hardcoded) + true // wipe_cutscene_final (hardcoded) +}; + //-------------------------------------------------------------------------- // SCREEN WIPE PACKAGE //-------------------------------------------------------------------------- boolean WipeInAction = false; +UINT8 g_wipemode = 0; +UINT8 g_wipetype = 0; +UINT8 g_wipeframe = 0; +boolean g_wipereverse = false; +boolean g_wipeskiprender = false; +boolean g_wipeencorewiggle = false; boolean WipeStageTitle = false; INT32 lastwipetic = 0; @@ -189,152 +327,6 @@ static fademask_t *F_GetFadeMask(UINT8 masknum, UINT8 scrnnum) { return NULL; } -/** Wipe ticker - * - * \param fademask pixels to change - */ -static void F_DoWipe(fademask_t *fademask, lighttable_t *fadecolormap, boolean reverse) -{ - // Software mask wipe -- optimized; though it might not look like it! - // Okay, to save you wondering *how* this is more optimized than the simpler - // version that came before it... - // --- - // The previous code did two FixedMul calls for every single pixel on the - // screen, of which there are hundreds of thousands -- if not millions -- of. - // This worked fine for smaller screen sizes, but with excessively large - // (1920x1200) screens that meant 4 million+ calls out to FixedMul, and that - // would take /just/ long enough that fades would start to noticably lag. - // --- - // This code iterates over the fade mask's pixels instead of the screen's, - // and deals with drawing over each rectangular area before it moves on to - // the next pixel in the fade mask. As a result, it's more complex (and might - // look a little messy; sorry!) but it simultaneously runs at twice the speed. - // In addition, we precalculate all the X and Y positions that we need to draw - // from and to, so it uses a little extra memory, but again, helps it run faster. - // --- - // Sal: I kinda destroyed some of this code by introducing Genesis-style fades. - // A colormap can be provided in F_RunWipe, which the white/black values will be - // remapped to the appropriate entry in the fade colormap. - { - // wipe screen, start, end - UINT8 *w = wipe_scr; - const UINT8 *s = wipe_scr_start; - const UINT8 *e = wipe_scr_end; - - // first pixel for each screen - UINT8 *w_base = w; - const UINT8 *s_base = s; - const UINT8 *e_base = e; - - // mask data, end - UINT8 *transtbl; - const UINT8 *mask = fademask->mask; - const UINT8 *maskend = mask + fademask->size; - - // rectangle draw hints - UINT32 draw_linestart, draw_rowstart; - UINT32 draw_lineend, draw_rowend; - UINT32 draw_linestogo, draw_rowstogo; - - // rectangle coordinates, etc. - UINT16* scrxpos = (UINT16*)malloc((fademask->width + 1) * sizeof(UINT16)); - UINT16* scrypos = (UINT16*)malloc((fademask->height + 1) * sizeof(UINT16)); - UINT16 maskx, masky; - UINT32 relativepos; - - // --- - // Screw it, we do the fixed point math ourselves up front. - scrxpos[0] = 0; - for (relativepos = 0, maskx = 1; maskx < fademask->width; ++maskx) - scrxpos[maskx] = (relativepos += fademask->xscale)>>FRACBITS; - scrxpos[fademask->width] = vid.width; - - scrypos[0] = 0; - for (relativepos = 0, masky = 1; masky < fademask->height; ++masky) - scrypos[masky] = (relativepos += fademask->yscale)>>FRACBITS; - scrypos[fademask->height] = vid.height; - // --- - - maskx = masky = 0; - do - { - UINT8 m = *mask; - - draw_rowstart = scrxpos[maskx]; - draw_rowend = scrxpos[maskx + 1]; - draw_linestart = scrypos[masky]; - draw_lineend = scrypos[masky + 1]; - - relativepos = (draw_linestart * vid.width) + draw_rowstart; - draw_linestogo = draw_lineend - draw_linestart; - - if (reverse) - m = ((pallen-1) - m); - - if (m == 0) - { - // shortcut - memcpy source to work - while (draw_linestogo--) - { - M_Memcpy(w_base+relativepos, (reverse ? e_base : s_base)+relativepos, draw_rowend-draw_rowstart); - relativepos += vid.width; - } - } - else if (m >= (pallen-1)) - { - // shortcut - memcpy target to work - while (draw_linestogo--) - { - M_Memcpy(w_base+relativepos, (reverse ? s_base : e_base)+relativepos, draw_rowend-draw_rowstart); - relativepos += vid.width; - } - } - else - { - // pointer to transtable that this mask would use - transtbl = transtables + ((9 - m)<= fademask->width) - ++masky, maskx = 0; - } while (++mask < maskend); - - free(scrxpos); - free(scrypos); - } -} #endif /** Save the "before" screen of a wipe. @@ -351,6 +343,7 @@ void F_WipeStartScreen(void) #endif wipe_scr_start = screens[3]; I_ReadScreen(wipe_scr_start); + I_FinishUpdateWipeStartScreen(); #endif } @@ -369,54 +362,10 @@ void F_WipeEndScreen(void) wipe_scr_end = screens[4]; I_ReadScreen(wipe_scr_end); V_DrawBlock(0, 0, 0, vid.width, vid.height, wipe_scr_start); + I_FinishUpdateWipeEndScreen(); #endif } -/** Wiggle post processor for encore wipes - */ -static void F_DoEncoreWiggle(UINT8 time) -{ - UINT8 *tmpscr = wipe_scr_start; - UINT8 *srcscr = wipe_scr; - angle_t disStart = (time * 128) & FINEMASK; - INT32 y, sine, newpix, scanline; - - for (y = 0; y < vid.height; y++) - { - sine = (FINESINE(disStart) * (time*12))>>FRACBITS; - scanline = y / vid.dupy; - if (scanline & 1) - sine = -sine; - newpix = abs(sine); - - if (sine < 0) - { - M_Memcpy(&tmpscr[(y*vid.width)+newpix], &srcscr[(y*vid.width)], vid.width-newpix); - - // Cleanup edge - while (newpix) - { - tmpscr[(y*vid.width)+newpix] = srcscr[(y*vid.width)]; - newpix--; - } - } - else - { - M_Memcpy(&tmpscr[(y*vid.width)], &srcscr[(y*vid.width) + sine], vid.width-newpix); - - // Cleanup edge - while (newpix) - { - tmpscr[(y*vid.width) + vid.width - newpix] = srcscr[(y*vid.width) + (vid.width-1)]; - newpix--; - } - } - - disStart += (time*8); //the offset into the displacement map, increment each game loop - disStart &= FINEMASK; //clip it to FINEMASK - } -} - /** Draw the stage title. */ void F_WipeStageTitle(void) @@ -432,9 +381,10 @@ void F_WipeStageTitle(void) /** After setting up the screens you want to wipe, * calling this will do a 'typical' wipe. */ -void F_RunWipe(UINT8 wipetype, boolean drawMenu, const char *colormap, boolean reverse, boolean encorewiggle) +void F_RunWipe(UINT8 wipemode, UINT8 wipetype, boolean drawMenu, const char *colormap, boolean reverse, boolean encorewiggle) { #ifdef NOWIPE + (void)wipemode; (void)wipetype; (void)drawMenu; (void)colormap; @@ -467,6 +417,7 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu, const char *colormap, boolean r // Init the wipe WipeInAction = true; + g_wipeskiprender = false; wipe_scr = screens[0]; // lastwipetic should either be 0 or the tic we last wiped @@ -494,14 +445,19 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu, const char *colormap, boolean r if (rendermode != render_none) //this allows F_RunWipe to be called in dedicated servers { - F_DoWipe(fmask, fcolor, reverse); + // F_DoWipe(fmask, fcolor, reverse); + g_wipemode = wipemode; + g_wipetype = wipetype; + g_wipeframe = wipeframe - 1; + g_wipereverse = reverse; if (encorewiggle) { -#ifdef HWRENDER - if (rendermode != render_opengl) -#endif - F_DoEncoreWiggle(wipeframe); + g_wipeencorewiggle = wipeframe - 1; + } + else + { + g_wipeencorewiggle = 0; } } @@ -519,15 +475,24 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu, const char *colormap, boolean r #endif } - I_FinishUpdate(); // page flip or blit buffer + I_FinishUpdateWipe(); // page flip or blit buffer - if (moviemode) - M_SaveFrame(); + if (rendermode != render_none) + { + // Skip subsequent renders until the end of the wipe to preserve the current frame. + g_wipeskiprender = true; + } + +#ifdef HWRENDER + if (moviemode && rendermode == render_opengl) + M_LegacySaveFrame(); +#endif NetKeepAlive(); // Update the network so we don't cause timeouts } WipeInAction = false; + g_wipeskiprender = false; if (fcolor) { @@ -585,3 +550,24 @@ boolean F_WipeExists(UINT8 wipetype) return !(lumpnum == LUMPERROR); #endif } + +boolean F_WipeIsToBlack(UINT8 wipemode) +{ + return g_wipedef_toblack[wipemode]; +} + +boolean F_WipeIsToWhite(UINT8 wipemode) +{ + return g_wipedef_towhite[wipemode]; +} + +boolean F_WipeIsToInvert(UINT8 wipemode) +{ + return g_wipedef_toinvert[wipemode]; +} + +boolean F_WipeIsCrossfade(UINT8 wipemode) +{ + return g_wipedef_crossfade[wipemode]; +} + diff --git a/src/g_demo.c b/src/g_demo.c index c94386d31..174511e4d 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -1184,7 +1184,7 @@ void G_GhostTicker(void) for(g = ghosts, p = NULL; g; g = g->next) { // Skip normal demo data. - UINT8 ziptic = READUINT8(g->p); + UINT16 ziptic = READUINT8(g->p); UINT8 xziptic = 0; while (ziptic != DW_END) // Get rid of extradata stuff @@ -1225,12 +1225,14 @@ void G_GhostTicker(void) ziptic = READUINT8(g->p); } - ziptic = READUINT8(g->p); + ziptic = READUINT16(g->p); if (ziptic & ZT_FWD) g->p++; if (ziptic & ZT_TURNING) g->p += 2; + if (ziptic & ZT_ANGLE) + g->p += 2; if (ziptic & ZT_THROWDIR) g->p += 2; if (ziptic & ZT_BUTTONS) diff --git a/src/g_game.c b/src/g_game.c index 136ca8a1d..c858bc85e 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -61,6 +61,7 @@ #include "k_specialstage.h" #include "k_bot.h" #include "doomstat.h" +#include "k_director.h" #ifdef HAVE_DISCORDRPC #include "discord.h" @@ -1164,6 +1165,37 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer) goto aftercmdinput; } + if (displayplayers[forplayer] != g_localplayers[forplayer]) + { + if (M_MenuButtonPressed(forplayer, MBT_A)) + { + G_AdjustView(ssplayer, 1, true); + K_ToggleDirector(false); + } + + if (M_MenuButtonPressed(forplayer, MBT_X)) + { + G_AdjustView(ssplayer, -1, true); + K_ToggleDirector(false); + } + + if (player->spectator == true) + { + // duplication of fire + if (G_PlayerInputDown(forplayer, gc_item, 0)) + { + cmd->buttons |= BT_ATTACK; + } + + if (M_MenuButtonPressed(forplayer, MBT_R)) + { + K_ToggleDirector(true); + } + } + + goto aftercmdinput; + } + if (K_PlayerUsesBotMovement(player)) { // Bot ticcmd is generated by K_BuildBotTiccmd @@ -1352,16 +1384,6 @@ aftercmdinput: cmd->throwdir = -KART_FULLTURN; G_DoAnglePrediction(cmd, realtics, ssplayer, player); - - // Reset away view if a command is given. - if ((cmd->forwardmove || cmd->buttons) - && !r_splitscreen && displayplayers[0] != consoleplayer && ssplayer == 1) - { - // Call ViewpointSwitch hooks here. - // The viewpoint was forcibly changed. - LUA_HookViewpointSwitch(player, &players[consoleplayer], true); - displayplayers[0] = consoleplayer; - } } ticcmd_t *G_CopyTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n) @@ -1556,10 +1578,10 @@ void G_PreLevelTitleCard(void) I_FinishUpdate(); // page flip or blit buffer NetKeepAlive(); // Prevent timeouts - if (moviemode) - M_SaveFrame(); - if (takescreenshot) // Only take screenshots after drawing. - M_DoScreenShot(); +#ifdef HWRENDER + if (moviemode && rendermode == render_opengl) + M_LegacySaveFrame(); +#endif while (!((nowtime = I_GetTime()) - lasttime)) { @@ -1695,44 +1717,8 @@ boolean G_Responder(event_t *ev) return true; // chat ate the event } - // allow spy mode changes even during the demo - if (gamestate == GS_LEVEL && ev->type == ev_keydown - && (ev->data1 == KEY_F12 /*|| ev->data1 == gamecontrol[0][gc_viewpoint][0] || ev->data1 == gamecontrol[0][gc_viewpoint][1]*/)) - { - if (!demo.playback && (r_splitscreen || !netgame)) - g_localplayers[0] = consoleplayer; - else - { - G_AdjustView(1, 1, true); - - // change statusbar also if playing back demo - if (demo.quitafterplaying) - ST_changeDemoView(); - - return true; - } - } - if (gamestate == GS_LEVEL && ev->type == ev_keydown && multiplayer && demo.playback && !demo.freecam) { - /* - if (ev->data1 == gamecontrol[1][gc_viewpoint][0] || ev->data1 == gamecontrol[1][gc_viewpoint][1]) - { - G_AdjustView(2, 1, true); - return true; - } - else if (ev->data1 == gamecontrol[2][gc_viewpoint][0] || ev->data1 == gamecontrol[2][gc_viewpoint][1]) - { - G_AdjustView(3, 1, true); - return true; - } - else if (ev->data1 == gamecontrol[3][gc_viewpoint][0] || ev->data1 == gamecontrol[3][gc_viewpoint][1]) - { - G_AdjustView(4, 1, true); - return true; - } - */ - // Allow pausing if ( //ev->data1 == gamecontrol[0][gc_pause][0] @@ -2026,6 +2012,10 @@ void G_ResetView(UINT8 viewnum, INT32 playernum, boolean onlyactive) if (viewnum == 1 && demo.playback) consoleplayer = displayplayers[0]; + + // change statusbar also if playing back demo + if (demo.quitafterplaying) + ST_changeDemoView(); } // @@ -3116,6 +3106,7 @@ void G_ExitLevel(void) // Remove CEcho text on round end. HU_ClearCEcho(); + HU_ClearTitlecardCEcho(); // Don't save demos immediately here! Let standings write first } @@ -4095,6 +4086,7 @@ void G_AfterIntermission(void) gamecomplete = 1; HU_ClearCEcho(); + HU_ClearTitlecardCEcho(); if (demo.playback) { @@ -4327,6 +4319,14 @@ void G_LoadGameSettings(void) #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual #define GD_VERSIONMINOR 1 // Change every format update +static const char *G_GameDataFolder(void) +{ + if (strcmp(srb2home,".")) + return srb2home; + else + return "the Ring Racers folder"; +} + // G_LoadGameData // Loads the main data file, which stores information such as emblems found, etc. void G_LoadGameData(void) @@ -4362,7 +4362,7 @@ void G_LoadGameData(void) { // Don't load, but do save. (essentially, reset) gamedata->loaded = true; - return; + return; } if (P_SaveBufferFromFile(&save, va(pandf, srb2home, gamedatafilename)) == false) @@ -4376,19 +4376,19 @@ void G_LoadGameData(void) versionID = READUINT32(save.p); if (versionID != GD_VERSIONCHECK) { - const char *gdfolder = "the Ring Racers folder"; - if (strcmp(srb2home,".")) - gdfolder = srb2home; + const char *gdfolder = G_GameDataFolder(); P_SaveBufferFree(&save); - I_Error("Game data is not for Ring Racers v2.0.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder); + I_Error("Game data is not for Ring Racers v2.0.\nDelete %s (maybe in %s) and try again.", gamedatafilename, gdfolder); } versionMinor = READUINT8(save.p); if (versionMinor > GD_VERSIONMINOR) { + const char *gdfolder = G_GameDataFolder(); + P_SaveBufferFree(&save); - I_Error("Game data is from the future! (expected %d, got %d)", GD_VERSIONMINOR, versionMinor); + I_Error("Game data is from the future! (expected %d, got %d)\nRename or delete %s (maybe in %s) and try again.", GD_VERSIONMINOR, versionMinor, gamedatafilename, gdfolder); } if (versionMinor == 0) { diff --git a/src/hu_stuff.c b/src/hu_stuff.c index 9f4b52cef..44264b48f 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -161,6 +161,11 @@ static tic_t cechotimer = 0; static tic_t cechoduration = 5*TICRATE; static INT32 cechoflags = 0; +static char tcechotext[1024]; // buffer for the titlecard text +static tic_t tcechotimer = 0; // goes up by 1 each frame this is active +static tic_t tcechoduration = 0; // Set automatically + + static tic_t resynch_ticker = 0; static huddrawlist_h luahuddrawlist_scores; @@ -1033,6 +1038,13 @@ void HU_Ticker(void) if (cechotimer) cechotimer--; + + if (tcechotimer) + { + tcechotimer++; + if (tcechotimer > tcechoduration) + tcechotimer = 0; + } if (gamestate != GS_LEVEL) { @@ -1996,6 +2008,66 @@ static void HU_DrawCEcho(void) } } +static void HU_DrawTitlecardCEcho(void) +{ + if (tcechotimer) + { + INT32 i = 0; + INT32 y = (BASEVIDHEIGHT/2)-16; + INT32 pnumlines = 0; + INT32 timeroffset = 0; + + char *line; + char *echoptr; + char temp[1024]; + + for (i = 0; tcechotext[i] != '\0'; ++i) + if (tcechotext[i] == '\\') + pnumlines++; + + y -= (pnumlines-1)*16; + + // Prevent crashing because I'm sick of this + if (y < 0) + { + CONS_Alert(CONS_WARNING, "CEcho contained too many lines, not displaying\n"); + cechotimer = 0; + return; + } + + strcpy(temp, tcechotext); + echoptr = &temp[0]; + + while (*echoptr != '\0') + { + INT32 w; + INT32 timer = (INT32)(tcechotimer - timeroffset); + + if (timer <= 0) + return; // we don't care. + + line = strchr(echoptr, '\\'); + + if (line == NULL) + break; + + *line = '\0'; + + w = V_TitleCardStringWidth(echoptr); + V_DrawTitleCardString(BASEVIDWIDTH/2 -w/2, y, echoptr, 0, false, timer, TICRATE*4); + + y += 32; + + // offset the timer for the next line. + timeroffset += strlen(echoptr); + + // set the ptr to the \0 we made and advance it because we don't want an empty string. + echoptr = line; + echoptr++; + } + } +} + // // demo info stuff // @@ -2137,6 +2209,9 @@ drawontop: if (cechotimer) HU_DrawCEcho(); + + if (tcechotimer) + HU_DrawTitlecardCEcho(); } //====================================================================== @@ -2590,3 +2665,22 @@ void HU_DoCEcho(const char *msg) cechotext[sizeof(cechotext) - 1] = '\0'; cechotimer = cechoduration; } + +// Simply set the timer to 0 to clear it. +// No need to bother clearing the buffer or anything. +void HU_ClearTitlecardCEcho(void) +{ + tcechotimer = 0; +} + +// Similar but for titlecard CEcho and also way less convoluted because I have no clue whatever the fuck they were trying above. +void HU_DoTitlecardCEcho(const char *msg) +{ + I_OutputMsg("%s\n", msg); // print to log + + strncpy(tcechotext, msg, sizeof(tcechotext)); + strncat(tcechotext, "\\", sizeof(tcechotext) - strlen(tcechotext) - 1); + tcechotext[sizeof(tcechotext) - 1] = '\0'; + tcechotimer = 1; + tcechoduration = TICRATE*6 + strlen(tcechotext); +} \ No newline at end of file diff --git a/src/hu_stuff.h b/src/hu_stuff.h index 1fc98d002..08b997bdc 100644 --- a/src/hu_stuff.h +++ b/src/hu_stuff.h @@ -154,6 +154,10 @@ void HU_SetCEchoDuration(INT32 seconds); void HU_SetCEchoFlags(INT32 flags); void HU_DoCEcho(const char *msg); +// Titlecard CECHO shite +void HU_DoTitlecardCEcho(const char *msg); +void HU_ClearTitlecardCEcho(void); + // Demo playback info extern UINT32 hu_demotime; extern UINT32 hu_demolap; diff --git a/src/hwr2/CMakeLists.txt b/src/hwr2/CMakeLists.txt index 504818105..0500c9ee4 100644 --- a/src/hwr2/CMakeLists.txt +++ b/src/hwr2/CMakeLists.txt @@ -1,8 +1,22 @@ target_sources(SRB2SDL2 PRIVATE + pass_blit_rect.cpp + pass_blit_rect.hpp pass_imgui.cpp pass_imgui.hpp + pass_manager.cpp + pass_manager.hpp + pass_postprocess.cpp + pass_postprocess.hpp + pass_resource_managers.cpp + pass_resource_managers.hpp + pass_screenshot.cpp + pass_screenshot.hpp pass_software.cpp pass_software.hpp + pass_twodee.cpp + pass_twodee.hpp pass.cpp pass.hpp + twodee.cpp + twodee.hpp ) diff --git a/src/hwr2/pass.cpp b/src/hwr2/pass.cpp index 48b331492..d20be7294 100644 --- a/src/hwr2/pass.cpp +++ b/src/hwr2/pass.cpp @@ -1,3 +1,15 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass.hpp" -srb2::hwr2::Pass::~Pass() = default; +using namespace srb2; +using namespace srb2::hwr2; + +Pass::~Pass() = default; diff --git a/src/hwr2/pass.hpp b/src/hwr2/pass.hpp index 2556bf8f8..a745bd12b 100644 --- a/src/hwr2/pass.hpp +++ b/src/hwr2/pass.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_HPP__ #define __SRB2_HWR2_PASS_HPP__ @@ -8,7 +17,9 @@ namespace srb2::hwr2 /// @brief A rendering pass which performs logic during each phase of a frame render. /// During rendering, all registered Pass's individual stages will be run together. -struct Pass { +class Pass +{ +public: virtual ~Pass(); /// @brief Perform rendering logic and create necessary GPU resources. diff --git a/src/hwr2/pass_blit_rect.cpp b/src/hwr2/pass_blit_rect.cpp new file mode 100644 index 000000000..56fb4f6dc --- /dev/null +++ b/src/hwr2/pass_blit_rect.cpp @@ -0,0 +1,209 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_blit_rect.hpp" + +#include + +#include + +#include "../cxxutil.hpp" + +using namespace srb2; +using namespace srb2::hwr2; +using namespace srb2::rhi; + +namespace +{ +struct BlitVertex +{ + float x = 0.f; + float y = 0.f; + float z = 0.f; + float u = 0.f; + float v = 0.f; +}; +} // namespace + +static const BlitVertex kVerts[] = + {{-.5f, -.5f, 0.f, 0.f, 0.f}, {.5f, -.5f, 0.f, 1.f, 0.f}, {-.5f, .5f, 0.f, 0.f, 1.f}, {.5f, .5f, 0.f, 1.f, 1.f}}; + +static const uint16_t kIndices[] = {0, 1, 2, 1, 3, 2}; + +/// @brief Pipeline used for paletted source textures. Requires the texture and the palette texture. +static const PipelineDesc kPalettedPipelineDescription = { + PipelineProgram::kUnshadedPaletted, + {{{sizeof(BlitVertex)}}, {{VertexAttributeName::kPosition, 0, 0}, {VertexAttributeName::kTexCoord0, 0, 12}}}, + {{{{UniformName::kProjection}}, {{UniformName::kModelView, UniformName::kTexCoord0Transform}}}}, + {{// R8 index texture + SamplerName::kSampler0, + // 256x1 palette texture + SamplerName::kSampler1}}, + std::nullopt, + {PixelFormat::kRGBA8, std::nullopt, {true, true, true, true}}, + PrimitiveType::kTriangles, + CullMode::kNone, + FaceWinding::kCounterClockwise, + {0.f, 0.f, 0.f, 1.f}}; + +/// @brief Pipeline used for non-paletted source textures. +static const PipelineDesc kUnshadedPipelineDescription = { + PipelineProgram::kUnshaded, + {{{sizeof(BlitVertex)}}, {{VertexAttributeName::kPosition, 0, 0}, {VertexAttributeName::kTexCoord0, 0, 12}}}, + {{{{UniformName::kProjection}}, {{UniformName::kModelView, UniformName::kTexCoord0Transform}}}}, + {{// RGB/A texture + SamplerName::kSampler0}}, + std::nullopt, + {PixelFormat::kRGBA8, std::nullopt, {true, true, true, true}}, + PrimitiveType::kTriangles, + CullMode::kNone, + FaceWinding::kCounterClockwise, + {0.f, 0.f, 0.f, 1.f}}; + +BlitRectPass::BlitRectPass() : Pass() +{ +} + +BlitRectPass::BlitRectPass(bool output_clear) : Pass(), output_clear_(output_clear) +{ +} + +BlitRectPass::BlitRectPass(const std::shared_ptr& palette_mgr, bool output_clear) + : Pass(), output_clear_(output_clear), palette_mgr_(palette_mgr) +{ +} + +BlitRectPass::~BlitRectPass() = default; + +void BlitRectPass::prepass(Rhi& rhi) +{ + if (!pipeline_) + { + if (palette_mgr_) + { + pipeline_ = rhi.create_pipeline(kPalettedPipelineDescription); + } + else + { + pipeline_ = rhi.create_pipeline(kUnshadedPipelineDescription); + } + } + + if (!quad_vbo_) + { + quad_vbo_ = rhi.create_buffer({sizeof(kVerts), BufferType::kVertexBuffer, BufferUsage::kImmutable}); + quad_vbo_needs_upload_ = true; + } + + if (!quad_ibo_) + { + quad_ibo_ = rhi.create_buffer({sizeof(kIndices), BufferType::kIndexBuffer, BufferUsage::kImmutable}); + quad_ibo_needs_upload_ = true; + } + + if (!render_pass_) + { + render_pass_ = rhi.create_render_pass( + {std::nullopt, + PixelFormat::kRGBA8, + output_clear_ ? AttachmentLoadOp::kClear : AttachmentLoadOp::kLoad, + AttachmentStoreOp::kStore} + ); + } +} + +void BlitRectPass::transfer(Rhi& rhi, Handle ctx) +{ + if (quad_vbo_needs_upload_ && quad_vbo_) + { + rhi.update_buffer_contents(ctx, quad_vbo_, 0, tcb::as_bytes(tcb::span(kVerts))); + quad_vbo_needs_upload_ = false; + } + + if (quad_ibo_needs_upload_ && quad_ibo_) + { + rhi.update_buffer_contents(ctx, quad_ibo_, 0, tcb::as_bytes(tcb::span(kIndices))); + quad_ibo_needs_upload_ = false; + } + + float aspect = 1.0; + float output_aspect = 1.0; + if (output_correct_aspect_) + { + aspect = static_cast(texture_width_) / static_cast(texture_height_); + output_aspect = static_cast(output_width_) / static_cast(output_height_); + } + bool taller = aspect > output_aspect; + + std::array g1_uniforms = {{ + // Projection + std::array, 4> { + {{taller ? 1.f : 1.f / output_aspect, 0.f, 0.f, 0.f}, + {0.f, taller ? -1.f / (1.f / output_aspect) : -1.f, 0.f, 0.f}, + {0.f, 0.f, 1.f, 0.f}, + {0.f, 0.f, 0.f, 1.f}}}, + }}; + + std::array g2_uniforms = { + {// ModelView + std::array, 4> { + {{taller ? 2.f : 2.f * aspect, 0.f, 0.f, 0.f}, + {0.f, taller ? 2.f * (1.f / aspect) : 2.f, 0.f, 0.f}, + {0.f, 0.f, 1.f, 0.f}, + {0.f, 0.f, 0.f, 1.f}}}, + // Texcoord0 Transform + std::array, 3> { + {{1.f, 0.f, 0.f}, {0.f, output_flip_ ? -1.f : 1.f, 0.f}, {0.f, 0.f, 1.f}}}}}; + + uniform_sets_[0] = rhi.create_uniform_set(ctx, {g1_uniforms}); + uniform_sets_[1] = rhi.create_uniform_set(ctx, {g2_uniforms}); + + std::array vbs = {{{0, quad_vbo_}}}; + if (palette_mgr_) + { + std::array tbs = { + {{rhi::SamplerName::kSampler0, texture_}, {rhi::SamplerName::kSampler1, palette_mgr_->palette()}}}; + binding_set_ = rhi.create_binding_set(ctx, pipeline_, {vbs, tbs}); + } + else + { + std::array tbs = {{{rhi::SamplerName::kSampler0, texture_}}}; + binding_set_ = rhi.create_binding_set(ctx, pipeline_, {vbs, tbs}); + } +} + +static constexpr const rhi::Color kClearColor = {0, 0, 0, 1}; + +void BlitRectPass::graphics(Rhi& rhi, Handle ctx) +{ + if (output_) + { + rhi.begin_render_pass(ctx, {render_pass_, output_, std::nullopt, kClearColor}); + } + else + { + rhi.begin_default_render_pass(ctx, output_clear_); + } + + rhi.bind_pipeline(ctx, pipeline_); + if (output_) + { + rhi.set_viewport(ctx, {0, 0, output_width_, output_height_}); + } + rhi.bind_uniform_set(ctx, 0, uniform_sets_[0]); + rhi.bind_uniform_set(ctx, 1, uniform_sets_[1]); + rhi.bind_binding_set(ctx, binding_set_); + rhi.bind_index_buffer(ctx, quad_ibo_); + rhi.draw_indexed(ctx, 6, 0); + rhi.end_render_pass(ctx); +} + +void BlitRectPass::postpass(Rhi& rhi) +{ +} diff --git a/src/hwr2/pass_blit_rect.hpp b/src/hwr2/pass_blit_rect.hpp new file mode 100644 index 000000000..010583c3e --- /dev/null +++ b/src/hwr2/pass_blit_rect.hpp @@ -0,0 +1,93 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_BLIT_RECT_HPP__ +#define __SRB2_HWR2_PASS_BLIT_RECT_HPP__ + +#include + +#include "../rhi/rhi.hpp" +#include "pass.hpp" +#include "pass_resource_managers.hpp" + +namespace srb2::hwr2 +{ + +/// @brief A render pass which blits a rect using a source texture or textures. +class BlitRectPass final : public Pass +{ + rhi::Handle pipeline_; + rhi::Handle texture_; + uint32_t texture_width_ = 0; + uint32_t texture_height_ = 0; + rhi::Handle output_; + uint32_t output_width_ = 0; + uint32_t output_height_ = 0; + bool output_correct_aspect_ = false; + bool output_clear_ = false; + bool output_flip_ = false; + rhi::Handle render_pass_; + rhi::Handle quad_vbo_; + rhi::Handle quad_ibo_; + std::array, 2> uniform_sets_; + rhi::Handle binding_set_; + + bool quad_vbo_needs_upload_ = false; + bool quad_ibo_needs_upload_ = false; + + // The presence of a palette manager indicates that the source texture will be paletted. This can't be changed. + std::shared_ptr palette_mgr_; + +public: + BlitRectPass(); + BlitRectPass(bool output_clear); + BlitRectPass(const std::shared_ptr& palette_mgr, bool output_clear); + virtual ~BlitRectPass(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + /// @brief Set the next blit texture. Don't call during graphics phase! + /// @param texture the texture to use when blitting + /// @param width texture width + /// @param height texture height + void set_texture(rhi::Handle texture, uint32_t width, uint32_t height) noexcept + { + texture_ = texture; + texture_width_ = width; + texture_height_ = height; + } + + /// @brief Set the next output texture. Don't call during graphics phase! + /// @param texture the texture to use as a color buffer + /// @param width texture width + /// @param height texture height + void set_output( + rhi::Handle color, + uint32_t width, + uint32_t height, + bool correct_aspect, + bool flip + ) noexcept + { + output_ = color; + output_width_ = width; + output_height_ = height; + output_correct_aspect_ = correct_aspect; + output_flip_ = flip; + } + + void clear_output(bool clear) noexcept { output_clear_ = clear; } +}; + +} // namespace srb2::hwr2 + +#endif // __SRB2_HWR2_PASS_SOFTWARE_HPP__ diff --git a/src/hwr2/pass_imgui.cpp b/src/hwr2/pass_imgui.cpp index 6d8e02861..e0d1d3cb6 100644 --- a/src/hwr2/pass_imgui.cpp +++ b/src/hwr2/pass_imgui.cpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_imgui.hpp" #include @@ -8,48 +17,32 @@ using namespace srb2; using namespace srb2::hwr2; using namespace srb2::rhi; -static const PipelineDesc kPipelineDesc = -{ +static const PipelineDesc kPipelineDesc = { PipelineProgram::kUnshaded, - { - { - {sizeof(ImDrawVert)} - }, - { - {VertexAttributeName::kPosition, 0, 0}, - {VertexAttributeName::kTexCoord0, 0, 12}, - {VertexAttributeName::kColor, 0, 24} - } - }, - {{ - {{UniformName::kProjection}}, - {{UniformName::kModelView, UniformName::kTexCoord0Transform}} - }}, - {{ - SamplerName::kSampler0 - }}, - PipelineDepthAttachmentDesc { - PixelFormat::kDepth16, - CompareFunc::kAlways, - true - }, - { - PixelFormat::kRGBA8, - BlendDesc { - BlendFactor::kSourceAlpha, - BlendFactor::kOneMinusSourceAlpha, - BlendFunction::kAdd, - BlendFactor::kOne, - BlendFactor::kOneMinusSourceAlpha, - BlendFunction::kAdd - }, - {true, true, true, true} - }, + {{{sizeof(ImDrawVert)}}, + {{VertexAttributeName::kPosition, 0, 0}, + {VertexAttributeName::kTexCoord0, 0, 12}, + {VertexAttributeName::kColor, 0, 24}}}, + {{{{UniformName::kProjection}}, {{UniformName::kModelView, UniformName::kTexCoord0Transform}}}}, + {{SamplerName::kSampler0}}, + PipelineDepthAttachmentDesc {PixelFormat::kDepth16, CompareFunc::kAlways, true}, + {PixelFormat::kRGBA8, + BlendDesc { + BlendFactor::kSourceAlpha, + BlendFactor::kOneMinusSourceAlpha, + BlendFunction::kAdd, + BlendFactor::kOne, + BlendFactor::kOneMinusSourceAlpha, + BlendFunction::kAdd}, + {true, true, true, true}}, PrimitiveType::kTriangles, CullMode::kNone, FaceWinding::kCounterClockwise, - {0.f, 0.f, 0.f, 1.f} -}; + {0.f, 0.f, 0.f, 1.f}}; + +ImguiPass::ImguiPass() : Pass() +{ +} ImguiPass::~ImguiPass() = default; @@ -86,18 +79,10 @@ void ImguiPass::prepass(Rhi& rhi) for (auto list : draw_lists) { Handle vbo = rhi.create_buffer( - { - static_cast(list->VtxBuffer.size_in_bytes()), - BufferType::kVertexBuffer, - BufferUsage::kImmutable - } + {static_cast(list->VtxBuffer.size_in_bytes()), BufferType::kVertexBuffer, BufferUsage::kImmutable} ); Handle ibo = rhi.create_buffer( - { - static_cast(list->IdxBuffer.size_in_bytes()), - BufferType::kIndexBuffer, - BufferUsage::kImmutable - } + {static_cast(list->IdxBuffer.size_in_bytes()), BufferType::kIndexBuffer, BufferUsage::kImmutable} ); DrawList hwr2_list; @@ -126,13 +111,11 @@ void ImguiPass::prepass(Rhi& rhi) draw_cmd.v_offset = cmd.VtxOffset; draw_cmd.i_offset = cmd.IdxOffset; draw_cmd.elems = cmd.ElemCount; - draw_cmd.clip = - { + draw_cmd.clip = { static_cast(clip_min.x), static_cast((data->DisplaySize.y * data->FramebufferScale.y) - clip_max.y), static_cast(clip_max.x - clip_min.x), - static_cast(clip_max.y - clip_min.y) - }; + static_cast(clip_max.y - clip_min.y)}; hwr2_list.cmds.push_back(std::move(draw_cmd)); } draw_lists_.push_back(std::move(hwr2_list)); @@ -179,35 +162,20 @@ void ImguiPass::transfer(Rhi& rhi, Handle ctx) rhi.update_buffer_contents(ctx, ibo, 0, tcb::as_bytes(index_span)); // Uniform sets - std::array g1_uniforms = - {{ + std::array g1_uniforms = {{ // Projection - std::array, 4> - {{ - {2.f / vid.realwidth, 0.f, 0.f, 0.f}, - {0.f, 2.f / vid.realheight, 0.f, 0.f}, - {0.f, 0.f, 1.f, 0.f}, - {-1.f, 1.f, 0.f, 1.f} - }}, - }}; - std::array g2_uniforms = - {{ - // ModelView - std::array, 4> - {{ - {1.f, 0.f, 0.f, 0.f}, - {0.f, -1.f, 0.f, 0.f}, - {0.f, 0.f, 1.f, 0.f}, - {0.f, 0, 0.f, 1.f} - }}, - // Texcoord0 Transform - std::array, 3> - {{ - {1.f, 0.f, 0.f}, - {0.f, 1.f, 0.f}, - {0.f, 0.f, 1.f} - }} + std::array, 4> { + {{2.f / vid.realwidth, 0.f, 0.f, 0.f}, + {0.f, 2.f / vid.realheight, 0.f, 0.f}, + {0.f, 0.f, 1.f, 0.f}, + {-1.f, 1.f, 0.f, 1.f}}}, }}; + std::array g2_uniforms = { + {// ModelView + std::array, 4> { + {{1.f, 0.f, 0.f, 0.f}, {0.f, -1.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}, {0.f, 0, 0.f, 1.f}}}, + // Texcoord0 Transform + std::array, 3> {{{1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}}}}}; Handle us_1 = rhi.create_uniform_set(ctx, {g1_uniforms}); Handle us_2 = rhi.create_uniform_set(ctx, {g2_uniforms}); diff --git a/src/hwr2/pass_imgui.hpp b/src/hwr2/pass_imgui.hpp index 280e7fc9c..91d2afe20 100644 --- a/src/hwr2/pass_imgui.hpp +++ b/src/hwr2/pass_imgui.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_IMGUI_HPP__ #define __SRB2_HWR2_PASS_IMGUI_HPP__ @@ -9,7 +18,7 @@ namespace srb2::hwr2 { -class ImguiPass : public Pass +class ImguiPass final : public Pass { struct DrawCmd { @@ -36,6 +45,7 @@ class ImguiPass : public Pass std::vector draw_lists_; public: + ImguiPass(); virtual ~ImguiPass(); virtual void prepass(rhi::Rhi& rhi) override; diff --git a/src/hwr2/pass_manager.cpp b/src/hwr2/pass_manager.cpp new file mode 100644 index 000000000..a6bc386a7 --- /dev/null +++ b/src/hwr2/pass_manager.cpp @@ -0,0 +1,191 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_manager.hpp" + +using namespace srb2; +using namespace srb2::hwr2; +using namespace srb2::rhi; + +namespace +{ + +class LambdaPass final : public Pass +{ + PassManager* mgr_; + std::function prepass_func_; + std::function postpass_func_; + +public: + LambdaPass(PassManager* mgr, std::function prepass_func); + LambdaPass( + PassManager* mgr, + std::function prepass_func, + std::function postpass_func + ); + virtual ~LambdaPass(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; +}; + +} // namespace + +LambdaPass::LambdaPass(PassManager* mgr, std::function prepass_func) + : mgr_(mgr), prepass_func_(prepass_func) +{ +} + +LambdaPass::LambdaPass( + PassManager* mgr, + std::function prepass_func, + std::function postpass_func +) + : mgr_(mgr), prepass_func_(prepass_func), postpass_func_(postpass_func) +{ +} + +LambdaPass::~LambdaPass() = default; + +void LambdaPass::prepass(Rhi& rhi) +{ + if (prepass_func_) + { + (prepass_func_)(*mgr_, rhi); + } +} + +void LambdaPass::transfer(Rhi&, Handle) +{ +} + +void LambdaPass::graphics(Rhi&, Handle) +{ +} + +void LambdaPass::postpass(Rhi& rhi) +{ + if (postpass_func_) + { + (postpass_func_)(*mgr_, rhi); + } +} + +PassManager::PassManager() = default; +PassManager::PassManager(const PassManager&) = default; +PassManager& PassManager::operator=(const PassManager&) = default; + +void PassManager::insert(const std::string& name, std::shared_ptr pass) +{ + SRB2_ASSERT(pass_by_name_.find(name) == pass_by_name_.end()); + + std::size_t index = passes_.size(); + passes_.push_back(PassManagerEntry {name, pass, true}); + pass_by_name_.insert({name, index}); +} + +void PassManager::insert(const std::string& name, std::function prepass_func) +{ + insert(std::forward(name), std::make_shared(LambdaPass {this, prepass_func})); +} + +void PassManager::insert( + const std::string& name, + std::function prepass_func, + std::function postpass_func +) +{ + insert( + std::forward(name), + std::make_shared(LambdaPass {this, prepass_func, postpass_func}) + ); +} + +void PassManager::set_pass_enabled(const std::string& name, bool enabled) +{ + SRB2_ASSERT(pass_by_name_.find(name) != pass_by_name_.end()); + + passes_[pass_by_name_[name]].enabled = enabled; +} + +std::weak_ptr PassManager::for_name(const std::string& name) +{ + auto itr = pass_by_name_.find(name); + if (itr == pass_by_name_.end()) + { + return std::weak_ptr(); + } + return passes_[itr->second].pass; +} + +void PassManager::prepass(Rhi& rhi) +{ + for (auto& pass : passes_) + { + if (pass.enabled) + { + pass.pass->prepass(rhi); + } + } +} + +void PassManager::transfer(Rhi& rhi, Handle ctx) +{ + for (auto& pass : passes_) + { + if (pass.enabled) + { + pass.pass->transfer(rhi, ctx); + } + } +} + +void PassManager::graphics(Rhi& rhi, Handle ctx) +{ + for (auto& pass : passes_) + { + if (pass.enabled) + { + pass.pass->graphics(rhi, ctx); + } + } +} + +void PassManager::postpass(Rhi& rhi) +{ + for (auto& pass : passes_) + { + if (pass.enabled) + { + pass.pass->postpass(rhi); + } + } +} + +void PassManager::render(Rhi& rhi) +{ + if (passes_.empty()) + { + return; + } + + prepass(rhi); + + Handle tc = rhi.begin_transfer(); + transfer(rhi, tc); + rhi.end_transfer(tc); + + Handle gc = rhi.begin_graphics(); + graphics(rhi, gc); + rhi.end_graphics(gc); + + postpass(rhi); +} diff --git a/src/hwr2/pass_manager.hpp b/src/hwr2/pass_manager.hpp new file mode 100644 index 000000000..c462dd395 --- /dev/null +++ b/src/hwr2/pass_manager.hpp @@ -0,0 +1,65 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_MANAGER_HPP__ +#define __SRB2_HWR2_PASS_MANAGER_HPP__ + +#include +#include +#include +#include +#include +#include + +#include "../rhi/rhi.hpp" +#include "pass.hpp" + +namespace srb2::hwr2 +{ + +class PassManager final : public Pass +{ + struct PassManagerEntry + { + std::string name; + std::shared_ptr pass; + bool enabled; + }; + + std::unordered_map pass_by_name_; + std::vector passes_; + +public: + PassManager(); + PassManager(const PassManager&); + PassManager(PassManager&&) = delete; + PassManager& operator=(const PassManager&); + PassManager& operator=(PassManager&&) = delete; + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + void insert(const std::string& name, std::shared_ptr pass); + void insert(const std::string& name, std::function prepass_func); + void insert( + const std::string& name, + std::function prepass_func, + std::function postpass_func + ); + std::weak_ptr for_name(const std::string& name); + void set_pass_enabled(const std::string& name, bool enabled); + + void render(rhi::Rhi& rhi); +}; + +} // namespace srb2::hwr2 + +#endif // __SRB2_HWR2_PASS_MANAGER_HPP__ diff --git a/src/hwr2/pass_postprocess.cpp b/src/hwr2/pass_postprocess.cpp new file mode 100644 index 000000000..240ef1c47 --- /dev/null +++ b/src/hwr2/pass_postprocess.cpp @@ -0,0 +1,243 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_postprocess.hpp" + +#include + +#include +#include + +#include "../f_finale.h" +#include "../w_wad.h" + +using namespace srb2; +using namespace srb2::hwr2; +using namespace srb2::rhi; + +namespace +{ +struct PostprocessVertex +{ + float x; + float y; + float z; + float u; + float v; +}; + +static const PostprocessVertex kPostprocessVerts[] = + {{-.5f, -.5f, 0.f, 0.f, 0.f}, {.5f, -.5f, 0.f, 1.f, 0.f}, {-.5f, .5f, 0.f, 0.f, 1.f}, {.5f, .5f, 0.f, 1.f, 1.f}}; + +static const uint16_t kPostprocessIndices[] = {0, 1, 2, 1, 3, 2}; + +} // namespace + +static const PipelineDesc kWipePipelineDesc = { + PipelineProgram::kPostprocessWipe, + {{{sizeof(PostprocessVertex)}}, + { + {VertexAttributeName::kPosition, 0, 0}, + {VertexAttributeName::kTexCoord0, 0, 12}, + }}, + {{{{UniformName::kProjection, UniformName::kWipeColorizeMode, UniformName::kWipeEncoreSwizzle}}}}, + {{SamplerName::kSampler0, SamplerName::kSampler1, SamplerName::kSampler2}}, + std::nullopt, + {PixelFormat::kRGBA8, std::nullopt, {true, true, true, true}}, + PrimitiveType::kTriangles, + CullMode::kNone, + FaceWinding::kCounterClockwise, + {0.f, 0.f, 0.f, 1.f}}; + +PostprocessWipePass::PostprocessWipePass() +{ +} + +PostprocessWipePass::~PostprocessWipePass() = default; + +void PostprocessWipePass::prepass(Rhi& rhi) +{ + if (!render_pass_) + { + render_pass_ = rhi.create_render_pass( + {std::nullopt, PixelFormat::kRGBA8, AttachmentLoadOp::kLoad, AttachmentStoreOp::kStore} + ); + } + + if (!pipeline_) + { + pipeline_ = rhi.create_pipeline(kWipePipelineDesc); + } + + if (!vbo_) + { + vbo_ = rhi.create_buffer({sizeof(PostprocessVertex) * 4, BufferType::kVertexBuffer, BufferUsage::kImmutable}); + upload_vbo_ = true; + } + if (!ibo_) + { + ibo_ = rhi.create_buffer({2 * 6, BufferType::kIndexBuffer, BufferUsage::kImmutable}); + upload_ibo_ = true; + } + + uint32_t wipe_mode = g_wipemode; + uint32_t wipe_type = g_wipetype; + uint32_t wipe_frame = g_wipeframe; + bool wipe_reverse = g_wipereverse; + + wipe_color_mode_ = 0; // TODO 0 = modulate, 1 = invert, 2 = MD to black, 3 = MD to white + if (F_WipeIsToBlack(wipe_mode)) + { + wipe_color_mode_ = 2; + } + else if (F_WipeIsToWhite(wipe_mode)) + { + wipe_color_mode_ = 3; + } + else if (F_WipeIsToInvert(wipe_mode)) + { + wipe_color_mode_ = 1; + } + else if (F_WipeIsCrossfade(wipe_mode)) + { + wipe_color_mode_ = 0; + } + + wipe_swizzle_ = g_wipeencorewiggle; + + if (wipe_type >= 100 || wipe_frame >= 100) + { + return; + } + + std::string lumpname = fmt::format(FMT_STRING("FADE{:02d}{:02d}"), wipe_type, wipe_frame); + lumpnum_t mask_lump = W_CheckNumForName(lumpname.c_str()); + if (mask_lump == LUMPERROR) + { + return; + } + + std::size_t mask_lump_size = W_LumpLength(mask_lump); + switch (mask_lump_size) + { + case 256000: + mask_w_ = 640; + mask_h_ = 400; + break; + case 64000: + mask_w_ = 320; + mask_h_ = 200; + break; + case 16000: + mask_w_ = 160; + mask_h_ = 100; + break; + case 4000: + mask_w_ = 80; + mask_h_ = 50; + break; + default: + return; + } + + mask_data_.clear(); + mask_data_.resize(mask_lump_size, 0); + W_ReadLump(mask_lump, mask_data_.data()); + if (wipe_reverse) + { + for (auto& b : mask_data_) + { + b = 32 - b; + } + } + + wipe_tex_ = rhi.create_texture({TextureFormat::kLuminance, mask_w_, mask_h_}); +} + +void PostprocessWipePass::transfer(Rhi& rhi, Handle ctx) +{ + if (wipe_tex_ == kNullHandle) + { + return; + } + + if (start_ == kNullHandle || end_ == kNullHandle) + { + return; + } + + if (upload_vbo_) + { + rhi.update_buffer_contents(ctx, vbo_, 0, tcb::as_bytes(tcb::span(kPostprocessVerts))); + upload_vbo_ = false; + } + + if (upload_ibo_) + { + rhi.update_buffer_contents(ctx, ibo_, 0, tcb::as_bytes(tcb::span(kPostprocessIndices))); + upload_ibo_ = false; + } + + tcb::span data = tcb::as_bytes(tcb::span(mask_data_)); + rhi.update_texture(ctx, wipe_tex_, {0, 0, mask_w_, mask_h_}, PixelFormat::kR8, data); + + UniformVariant uniforms[] = { + {std::array, 4> { + {{2.f, 0.f, 0.f, 0.f}, {0.f, 2.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}, {0.f, 0.f, 0.f, 1.f}}}}, + {static_cast(wipe_color_mode_)}, + {static_cast(wipe_swizzle_)}}; + us_ = rhi.create_uniform_set(ctx, {tcb::span(uniforms)}); + + VertexAttributeBufferBinding vbos[] = {{0, vbo_}}; + TextureBinding tx[] = { + {SamplerName::kSampler0, start_}, + {SamplerName::kSampler1, end_}, + {SamplerName::kSampler2, wipe_tex_}}; + bs_ = rhi.create_binding_set(ctx, pipeline_, {vbos, tx}); +} + +void PostprocessWipePass::graphics(Rhi& rhi, Handle ctx) +{ + if (wipe_tex_ == kNullHandle) + { + return; + } + + if (target_) + { + rhi.begin_render_pass(ctx, {render_pass_, target_, std::nullopt, {0, 0, 0, 1}}); + } + else + { + rhi.begin_default_render_pass(ctx, false); + } + + rhi.bind_pipeline(ctx, pipeline_); + if (target_) + { + rhi.set_viewport(ctx, {0, 0, target_w_, target_h_}); + } + rhi.bind_uniform_set(ctx, 0, us_); + rhi.bind_binding_set(ctx, bs_); + rhi.bind_index_buffer(ctx, ibo_); + rhi.draw_indexed(ctx, 6, 0); + + rhi.end_render_pass(ctx); +} + +void PostprocessWipePass::postpass(Rhi& rhi) +{ + if (wipe_tex_) + { + rhi.destroy_texture(wipe_tex_); + wipe_tex_ = kNullHandle; + } + + mask_data_.clear(); +} diff --git a/src/hwr2/pass_postprocess.hpp b/src/hwr2/pass_postprocess.hpp new file mode 100644 index 000000000..ced81975c --- /dev/null +++ b/src/hwr2/pass_postprocess.hpp @@ -0,0 +1,70 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_POSTPROCESS_HPP__ +#define __SRB2_HWR2_PASS_POSTPROCESS_HPP__ + +#include "pass.hpp" + +#include + +namespace srb2::hwr2 +{ + +class PostprocessWipePass final : public Pass +{ + // Internal RHI resources + rhi::Handle render_pass_; + rhi::Handle pipeline_; + rhi::Handle vbo_; + bool upload_vbo_ = false; + rhi::Handle ibo_; + bool upload_ibo_ = false; + rhi::Handle us_; + rhi::Handle bs_; + rhi::Handle wipe_tex_; + int wipe_color_mode_ = 0; + int wipe_swizzle_ = 0; + + // Pass parameters + rhi::Handle start_; + rhi::Handle end_; + rhi::Handle target_; + uint32_t target_w_ = 0; + uint32_t target_h_ = 0; + + // Mask lump loading + std::vector mask_data_; + uint32_t mask_w_ = 0; + uint32_t mask_h_ = 0; + +public: + PostprocessWipePass(); + virtual ~PostprocessWipePass(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + void set_start(rhi::Handle start) noexcept { start_ = start; } + + void set_end(rhi::Handle end) noexcept { end_ = end; } + + void set_target(rhi::Handle target, uint32_t width, uint32_t height) noexcept + { + target_ = target; + target_w_ = width; + target_h_ = height; + } +}; + +} // namespace srb2::hwr2 + +#endif // __SRB2_HWR2_PASS_POSTPROCESS_HPP__ diff --git a/src/hwr2/pass_resource_managers.cpp b/src/hwr2/pass_resource_managers.cpp new file mode 100644 index 000000000..fb38dcdf4 --- /dev/null +++ b/src/hwr2/pass_resource_managers.cpp @@ -0,0 +1,285 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_resource_managers.hpp" + +#include +#include + +#include "../v_video.h" +#include "../z_zone.h" + +using namespace srb2; +using namespace srb2::hwr2; +using namespace srb2::rhi; + +FramebufferManager::FramebufferManager() : Pass() +{ +} + +FramebufferManager::~FramebufferManager() = default; + +void FramebufferManager::prepass(Rhi& rhi) +{ + uint32_t current_width = vid.width; + uint32_t current_height = vid.height; + + // Destroy the framebuffer textures if they exist and the video size changed + if (width_ != current_width || height_ != current_height) + { + if (main_color_ != kNullHandle) + { + rhi.destroy_texture(main_color_); + main_color_ = kNullHandle; + } + if (main_depth_ != kNullHandle) + { + rhi.destroy_renderbuffer(main_depth_); + main_depth_ = kNullHandle; + } + + if (post_colors_[0] != kNullHandle) + { + rhi.destroy_texture(post_colors_[0]); + post_colors_[0] = kNullHandle; + } + if (post_colors_[1] != kNullHandle) + { + rhi.destroy_texture(post_colors_[1]); + post_colors_[1] = kNullHandle; + } + if (wipe_start_color_ != kNullHandle) + { + rhi.destroy_texture(wipe_start_color_); + wipe_start_color_ = kNullHandle; + } + if (wipe_end_color_ != kNullHandle) + { + rhi.destroy_texture(wipe_end_color_); + wipe_end_color_ = kNullHandle; + } + } + width_ = current_width; + height_ = current_height; + + // Recreate the framebuffer textures + if (main_color_ == kNullHandle) + { + main_color_ = rhi.create_texture({TextureFormat::kRGBA, current_width, current_height}); + } + if (main_depth_ == kNullHandle) + { + main_depth_ = rhi.create_renderbuffer({PixelFormat::kDepth16, current_width, current_height}); + } + + if (post_colors_[0] == kNullHandle) + { + post_colors_[0] = rhi.create_texture({TextureFormat::kRGBA, current_width, current_height}); + } + if (post_colors_[1] == kNullHandle) + { + post_colors_[1] = rhi.create_texture({TextureFormat::kRGBA, current_width, current_height}); + } + + if (wipe_start_color_ == kNullHandle) + { + wipe_start_color_ = rhi.create_texture({TextureFormat::kRGBA, current_width, current_height}); + } + if (wipe_end_color_ == kNullHandle) + { + wipe_end_color_ = rhi.create_texture({TextureFormat::kRGBA, current_width, current_height}); + } +} + +void FramebufferManager::transfer(Rhi& rhi, Handle ctx) +{ +} + +void FramebufferManager::graphics(Rhi& rhi, Handle ctx) +{ +} + +void FramebufferManager::postpass(Rhi& rhi) +{ +} + +MainPaletteManager::MainPaletteManager() : Pass() +{ +} + +MainPaletteManager::~MainPaletteManager() = default; + +void MainPaletteManager::prepass(Rhi& rhi) +{ + if (!palette_) + { + palette_ = rhi.create_texture({TextureFormat::kRGBA, 256, 1}); + } +} + +void MainPaletteManager::transfer(Rhi& rhi, Handle ctx) +{ + std::array palette_32; + for (std::size_t i = 0; i < 256; i++) + { + palette_32[i] = V_GetColor(i).s; + } + rhi.update_texture(ctx, palette_, {0, 0, 256, 1}, PixelFormat::kRGBA8, tcb::as_bytes(tcb::span(palette_32))); +} + +void MainPaletteManager::graphics(Rhi& rhi, Handle ctx) +{ +} + +void MainPaletteManager::postpass(Rhi& rhi) +{ +} + +CommonResourcesManager::CommonResourcesManager() = default; +CommonResourcesManager::~CommonResourcesManager() = default; + +void CommonResourcesManager::prepass(Rhi& rhi) +{ + if (!init_) + { + black_ = rhi.create_texture({TextureFormat::kRGBA, 1, 1}); + white_ = rhi.create_texture({TextureFormat::kRGBA, 1, 1}); + transparent_ = rhi.create_texture({TextureFormat::kRGBA, 1, 1}); + } +} + +void CommonResourcesManager::transfer(Rhi& rhi, Handle ctx) +{ + if (!init_) + { + uint8_t black[4] = {0, 0, 0, 255}; + tcb::span black_bytes = tcb::as_bytes(tcb::span(black, 4)); + uint8_t white[4] = {255, 255, 255, 255}; + tcb::span white_bytes = tcb::as_bytes(tcb::span(white, 4)); + uint8_t transparent[4] = {0, 0, 0, 0}; + tcb::span transparent_bytes = tcb::as_bytes(tcb::span(transparent, 4)); + + rhi.update_texture(ctx, black_, {0, 0, 1, 1}, PixelFormat::kRGBA8, black_bytes); + rhi.update_texture(ctx, white_, {0, 0, 1, 1}, PixelFormat::kRGBA8, white_bytes); + rhi.update_texture(ctx, transparent_, {0, 0, 1, 1}, PixelFormat::kRGBA8, transparent_bytes); + } +} + +void CommonResourcesManager::graphics(Rhi& rhi, Handle ctx) +{ +} + +void CommonResourcesManager::postpass(Rhi& rhi) +{ + init_ = true; +} + +static uint32_t get_flat_size(lumpnum_t lump) +{ + SRB2_ASSERT(lump != LUMPERROR); + + std::size_t lumplength = W_LumpLength(lump); + if (lumplength == 0) + { + return 0; + } + + if ((lumplength & (lumplength - 1)) != 0) + { + // Lump length is not a power of two and therefore not a flat. + return 0; + } + uint32_t lumpsize = std::pow(2, std::log2(lumplength) / 2); + return lumpsize; +} + +FlatTextureManager::FlatTextureManager() : Pass() +{ +} + +FlatTextureManager::~FlatTextureManager() = default; + +void FlatTextureManager::prepass(Rhi& rhi) +{ +} + +void FlatTextureManager::transfer(Rhi& rhi, Handle ctx) +{ + std::vector> flat_data; + for (auto flat_lump : to_upload_) + { + flat_data.clear(); + Handle flat_texture = flats_[flat_lump]; + SRB2_ASSERT(flat_texture != kNullHandle); + std::size_t lump_length = W_LumpLength(flat_lump); + uint32_t flat_size = get_flat_size(flat_lump); + flat_data.reserve(flat_size * flat_size); + + const uint8_t* flat_memory = static_cast(W_CacheLumpNum(flat_lump, PU_PATCH)); + SRB2_ASSERT(flat_memory != nullptr); + + tcb::span flat_bytes = tcb::span(flat_memory, lump_length); + for (const uint8_t index : flat_bytes) + { + // The alpha/green channel is set to 0 if it's index 247; this is not usually used but fake floors can be + // masked sometimes, so we need to treat it as transparent when rendering them. + // See https://zdoom.org/wiki/Palette for remarks on fake 247 transparency + flat_data.push_back({index, index == 247 ? static_cast(0) : static_cast(255)}); + } + + // A flat size of 1 would end up being 2 bytes, so we need 2 more bytes to be unpack-aligned on texture upload + // Any other size would implicitly be aligned. + // Sure hope nobody tries to load any flats that are too big for the gpu! + if (flat_size == 1) + { + flat_data.push_back({0, 0}); + } + + tcb::span data_bytes = tcb::as_bytes(tcb::span(flat_data)); + rhi.update_texture(ctx, flat_texture, {0, 0, flat_size, flat_size}, rhi::PixelFormat::kRG8, data_bytes); + } + to_upload_.clear(); +} + +void FlatTextureManager::graphics(Rhi& rhi, Handle ctx) +{ +} + +void FlatTextureManager::postpass(Rhi& rhi) +{ +} + +Handle FlatTextureManager::find_or_create_indexed(Rhi& rhi, lumpnum_t lump) +{ + SRB2_ASSERT(lump != LUMPERROR); + + auto flat_itr = flats_.find(lump); + if (flat_itr != flats_.end()) + { + return flat_itr->second; + } + + uint32_t flat_size = get_flat_size(lump); + Handle new_tex = rhi.create_texture({TextureFormat::kLuminanceAlpha, flat_size, flat_size}); + flats_.insert({lump, new_tex}); + to_upload_.push_back(lump); + return new_tex; +} + +Handle FlatTextureManager::find_indexed(lumpnum_t lump) const +{ + SRB2_ASSERT(lump != LUMPERROR); + + auto flat_itr = flats_.find(lump); + if (flat_itr != flats_.end()) + { + return flat_itr->second; + } + return kNullHandle; +} diff --git a/src/hwr2/pass_resource_managers.hpp b/src/hwr2/pass_resource_managers.hpp new file mode 100644 index 000000000..30c158572 --- /dev/null +++ b/src/hwr2/pass_resource_managers.hpp @@ -0,0 +1,150 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_RESOURCE_MANAGERS_HPP__ +#define __SRB2_HWR2_PASS_RESOURCE_MANAGERS_HPP__ + +#include +#include +#include +#include + +#include "pass.hpp" + +namespace srb2::hwr2 +{ + +class FramebufferManager final : public Pass +{ + rhi::Handle main_color_; + rhi::Handle main_depth_; + std::array, 2> post_colors_; + rhi::Handle wipe_start_color_; + rhi::Handle wipe_end_color_; + std::size_t post_index_ = 0; + std::size_t width_ = 0; + std::size_t height_ = 0; + bool first_postprocess_ = true; + +public: + FramebufferManager(); + virtual ~FramebufferManager(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + /// @brief Swap the current and previous postprocess FB textures. Use between pass prepass phases to alternate. + void swap_post() noexcept + { + post_index_ = post_index_ == 0 ? 1 : 0; + first_postprocess_ = false; + } + + void reset_post() noexcept { first_postprocess_ = true; } + + rhi::Handle main_color() const noexcept { return main_color_; } + rhi::Handle main_depth() const noexcept { return main_depth_; } + + rhi::Handle current_post_color() const noexcept { return post_colors_[post_index_]; } + + rhi::Handle previous_post_color() const noexcept + { + if (first_postprocess_) + { + return main_color(); + } + return post_colors_[1 - post_index_]; + }; + + rhi::Handle wipe_start_color() const noexcept { return wipe_start_color_; } + rhi::Handle wipe_end_color() const noexcept { return wipe_end_color_; } + + std::size_t width() const noexcept { return width_; } + std::size_t height() const noexcept { return height_; } +}; + +class MainPaletteManager final : public Pass +{ + rhi::Handle palette_; + +public: + MainPaletteManager(); + virtual ~MainPaletteManager(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + rhi::Handle palette() const noexcept { return palette_; } +}; + +class CommonResourcesManager final : public Pass +{ + bool init_ = false; + rhi::Handle black_; + rhi::Handle white_; + rhi::Handle transparent_; + +public: + CommonResourcesManager(); + virtual ~CommonResourcesManager(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + rhi::Handle black() const noexcept { return black_; } + rhi::Handle white() const noexcept { return white_; } + rhi::Handle transparent() const noexcept { return transparent_; } +}; + +/* +A note to the reader: + +RHI/HWR2's architecture is intentionally decoupled in a data-oriented design fashion. Hash map lookups might technically +be slower than storing the RHI handle in a hypothetical Flat class object, but it frees us from worrying about the +validity of a given Handle when the RHI instance changes -- and it _can_, because this is designed to allow multiple +RHI backends -- because any given Pass must be disposed when the RHI changes. The implementation of I_FinishUpdate is +such that if the RHI is not the same as before, all passes must be reconstructed, and so we don't have to worry about +going around and resetting Handle references everywhere. If you're familiar with old GL, it's like decoupling GLmipmap_t +from patch_t. +*/ + +/// @brief Manages textures corresponding to specific flats indexed by lump number. +class FlatTextureManager final : public Pass +{ + std::unordered_map> flats_; + std::vector to_upload_; + std::vector> disposed_textures_; + +public: + FlatTextureManager(); + virtual ~FlatTextureManager(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + /// @brief Find the indexed texture for a given flat lump, or create one if it doesn't exist yet. Only call this + /// in prepass. + /// @param flat_lump + /// @return + rhi::Handle find_or_create_indexed(rhi::Rhi& rhi, lumpnum_t flat_lump); + + rhi::Handle find_indexed(lumpnum_t flat_lump) const; +}; + +} // namespace srb2::hwr2 + +#endif // __SRB2_HWR2_PASS_RESOURCE_MANAGERS_HPP__ diff --git a/src/hwr2/pass_screenshot.cpp b/src/hwr2/pass_screenshot.cpp new file mode 100644 index 000000000..64ed1e4be --- /dev/null +++ b/src/hwr2/pass_screenshot.cpp @@ -0,0 +1,77 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_screenshot.hpp" + +#include + +#include "../m_misc.h" + +using namespace srb2; +using namespace srb2::hwr2; +using namespace srb2::rhi; + +ScreenshotPass::ScreenshotPass() = default; +ScreenshotPass::~ScreenshotPass() = default; + +void ScreenshotPass::prepass(Rhi& rhi) +{ + if (!render_pass_) + { + render_pass_ = rhi.create_render_pass( + { + std::nullopt, + PixelFormat::kRGBA8, + AttachmentLoadOp::kLoad, + AttachmentStoreOp::kStore + } + ); + } + + doing_screenshot_ = takescreenshot || moviemode != MM_OFF; +} + +void ScreenshotPass::transfer(Rhi& rhi, Handle ctx) +{ +} + +void ScreenshotPass::graphics(Rhi& rhi, Handle ctx) +{ + if (!doing_screenshot_) + { + return; + } + + pixel_data_.clear(); + pixel_data_.resize(width_ * height_ * 3); // 3 bytes per pixel for RGB8 + + rhi.begin_render_pass(ctx, {render_pass_, source_, std::nullopt, {0.f, 0.f, 0.f, 0.f}}); + rhi.read_pixels(ctx, {0, 0, width_, height_}, PixelFormat::kRGB8, tcb::as_writable_bytes(tcb::span(pixel_data_))); + rhi.end_render_pass(ctx); +} + +void ScreenshotPass::postpass(Rhi& rhi) +{ + if (!doing_screenshot_) + { + return; + } + + if (takescreenshot) + { + M_DoScreenShot(width_, height_, tcb::as_bytes(tcb::span(pixel_data_))); + } + + if (moviemode != MM_OFF) + { + M_SaveFrame(width_, height_, tcb::as_bytes(tcb::span(pixel_data_))); + } + + doing_screenshot_ = false; +} diff --git a/src/hwr2/pass_screenshot.hpp b/src/hwr2/pass_screenshot.hpp new file mode 100644 index 000000000..79de8b654 --- /dev/null +++ b/src/hwr2/pass_screenshot.hpp @@ -0,0 +1,49 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_SCREENSHOT_HPP__ +#define __SRB2_HWR2_PASS_SCREENSHOT_HPP__ + +#include +#include + +#include "pass.hpp" + +namespace srb2::hwr2 +{ + +class ScreenshotPass : public Pass +{ + bool doing_screenshot_ = false; + rhi::Handle source_; + rhi::Handle render_pass_; + std::vector pixel_data_; + uint32_t width_ = 0; + uint32_t height_ = 0; + +public: + ScreenshotPass(); + virtual ~ScreenshotPass(); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; + + void set_source(rhi::Handle source, uint32_t width, uint32_t height) + { + source_ = source; + width_ = width; + height_ = height; + } +}; + +} // namespace srb2::hwr2 + +#endif // __SRB2_HWR2_PASS_SCREENSHOT_HPP__ diff --git a/src/hwr2/pass_software.cpp b/src/hwr2/pass_software.cpp index a94b40169..a95576f34 100644 --- a/src/hwr2/pass_software.cpp +++ b/src/hwr2/pass_software.cpp @@ -1,94 +1,34 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_software.hpp" -#include +#include "../i_video.h" +#include "../v_video.h" -#include - -#include "../cxxutil.hpp" #include "../d_netcmd.h" #ifdef HAVE_DISCORDRPC #include "../discord.h" #endif #include "../doomstat.h" +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #include "../m_avrecorder.h" +#endif #include "../st_stuff.h" #include "../s_sound.h" +#include "../st_stuff.h" #include "../v_video.h" using namespace srb2; using namespace srb2::hwr2; using namespace srb2::rhi; -SoftwareBlitPass::~SoftwareBlitPass() = default; - -namespace -{ -struct SwBlitVertex -{ - float x = 0.f; - float y = 0.f; - float z = 0.f; - float u = 0.f; - float v = 0.f; -}; -} // namespace - -static const SwBlitVertex kVerts[] = -{ - {-.5f, -.5f, 0.f, 0.f, 0.f}, - {.5f, -.5f, 0.f, 1.f, 0.f}, - {-.5f, .5f, 0.f, 0.f, 1.f}, - {.5f, .5f, 0.f, 1.f, 1.f} -}; - -static const uint16_t kIndices[] = {0, 1, 2, 1, 3, 2}; - -static const PipelineDesc kPipelineDescription = -{ - PipelineProgram::kUnshadedPaletted, - { - { - {sizeof(SwBlitVertex)} - }, - { - {VertexAttributeName::kPosition, 0, 0}, - {VertexAttributeName::kTexCoord0, 0, 12} - } - }, - {{ - {{UniformName::kProjection}}, - {{UniformName::kModelView, UniformName::kTexCoord0Transform}} - }}, - {{ - // R8 index texture - SamplerName::kSampler0, - // 256x1 palette texture - SamplerName::kSampler1 - }}, - std::nullopt, - { - PixelFormat::kRGBA8, - std::nullopt, - {true, true, true, true} - }, - PrimitiveType::kTriangles, - CullMode::kNone, - FaceWinding::kCounterClockwise, - {0.f, 0.f, 0.f, 1.f} -}; - -static uint32_t next_pow_of_2(uint32_t in) -{ - in--; - in |= in >> 1; - in |= in >> 2; - in |= in >> 4; - in |= in >> 8; - in |= in >> 16; - in++; - return in; -} - static void temp_legacy_finishupdate_draws() { SCR_CalculateFPS(); @@ -100,8 +40,7 @@ static void temp_legacy_finishupdate_draws() if (cv_ticrate.value) SCR_DisplayTicRate(); - if (cv_showping.value && netgame && - ( consoleplayer != serverplayer || ! server_lagless )) + if (cv_showping.value && netgame && (consoleplayer != serverplayer || !server_lagless)) { if (server_lagless) { @@ -110,11 +49,8 @@ static void temp_legacy_finishupdate_draws() } else { - for ( - int player = 1; - player < MAXPLAYERS; - player++ - ){ + for (int player = 1; player < MAXPLAYERS; player++) + { if (D_IsPlayerHumanAndGaming(player)) { SCR_DisplayLocalPing(); @@ -125,8 +61,9 @@ static void temp_legacy_finishupdate_draws() } if (cv_mindelay.value && consoleplayer == serverplayer && Playing()) SCR_DisplayLocalPing(); - +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES M_AVRecorder_DrawFrameRate(); +#endif } if (marathonmode) @@ -142,149 +79,83 @@ static void temp_legacy_finishupdate_draws() #endif } -void SoftwareBlitPass::prepass(Rhi& rhi) +SoftwarePass::SoftwarePass() : Pass() { - if (!pipeline_) +} + +SoftwarePass::~SoftwarePass() = default; + +void SoftwarePass::prepass(Rhi& rhi) +{ + if (rendermode != render_soft) { - pipeline_ = rhi.create_pipeline(kPipelineDescription); + return; } - if (!quad_vbo_) - { - quad_vbo_ = rhi.create_buffer({sizeof(kVerts), BufferType::kVertexBuffer, BufferUsage::kImmutable}); - quad_vbo_needs_upload_ = true; - } - - if (!quad_ibo_) - { - quad_ibo_ = rhi.create_buffer({sizeof(kIndices), BufferType::kIndexBuffer, BufferUsage::kImmutable}); - quad_ibo_needs_upload_ = true; - } + // Render the player views... or not yet? Needs to be moved out of D_Display in d_main.c + // Assume it's already been done and vid.buffer contains the composited splitscreen view. + // In the future though, we will want to treat each player viewport separately for postprocessing. temp_legacy_finishupdate_draws(); - uint32_t vid_width = static_cast(vid.width); - uint32_t vid_height = static_cast(vid.height); - - if (screen_tex_ && (screen_tex_width_ < vid_width || screen_tex_height_ < vid_height)) + // Prepare RHI resources + if (screen_texture_ && (static_cast(width_) != vid.width || static_cast(height_) != vid.height)) { - rhi.destroy_texture(screen_tex_); - screen_tex_ = kNullHandle; + // Mode changed, recreate texture + rhi.destroy_texture(screen_texture_); + screen_texture_ = kNullHandle; } - if (!screen_tex_) + width_ = vid.width; + height_ = vid.height; + + if (!screen_texture_) { - screen_tex_width_ = next_pow_of_2(vid_width); - screen_tex_height_ = next_pow_of_2(vid_height); - screen_tex_ = rhi.create_texture({TextureFormat::kLuminance, screen_tex_width_, screen_tex_height_}); + screen_texture_ = rhi.create_texture({TextureFormat::kLuminance, width_, height_}); } - if (!palette_tex_) + // If the screen width won't fit the unpack alignment, we need to copy the screen. + if (width_ % kPixelRowUnpackAlignment > 0) { - palette_tex_ = rhi.create_texture({TextureFormat::kRGBA, 256, 1}); + std::size_t padded_width = (width_ + (kPixelRowUnpackAlignment - 1)) & !kPixelRowUnpackAlignment; + copy_buffer_.clear(); + copy_buffer_.reserve(padded_width * height_); + for (std::size_t y = 0; y < height_; y++) + { + for (std::size_t x = 0; x < width_; x++) + { + copy_buffer_.push_back(vid.buffer[(width_ * y) + x]); + } + + // Padding to unpack alignment + for (std::size_t i = 0; i < padded_width - width_; i++) + { + copy_buffer_.push_back(0); + } + } } } -void SoftwareBlitPass::upload_screen(Rhi& rhi, Handle ctx) +void SoftwarePass::transfer(Rhi& rhi, Handle ctx) { - rhi::Rect screen_rect = { - 0, - 0, - static_cast(vid.width), - static_cast(vid.height) - }; - - tcb::span screen_span = tcb::span(vid.buffer, static_cast(vid.width * vid.height)); - rhi.update_texture(ctx, screen_tex_, screen_rect, rhi::PixelFormat::kR8, tcb::as_bytes(screen_span)); -} - -void SoftwareBlitPass::upload_palette(Rhi& rhi, Handle ctx) -{ - // Unfortunately, pMasterPalette must be swizzled to get a linear layout. - // Maybe some adjustments to palette storage can make this a straight upload. - std::array palette_32; - for (size_t i = 0; i < 256; i++) + // Upload screen + tcb::span screen_span; + if (width_ % kPixelRowUnpackAlignment > 0) { - palette_32[i] = pMasterPalette[i].s; + screen_span = tcb::as_bytes(tcb::span(copy_buffer_)); } - rhi.update_texture(ctx, palette_tex_, {0, 0, 256, 1}, rhi::PixelFormat::kRGBA8, tcb::as_bytes(tcb::span(palette_32))); -} - -void SoftwareBlitPass::transfer(Rhi& rhi, Handle ctx) -{ - if (quad_vbo_needs_upload_ && quad_vbo_) + else { - rhi.update_buffer_contents(ctx, quad_vbo_, 0, tcb::as_bytes(tcb::span(kVerts))); - quad_vbo_needs_upload_ = false; + screen_span = tcb::as_bytes(tcb::span(vid.buffer, width_ * height_)); } - if (quad_ibo_needs_upload_ && quad_ibo_) - { - rhi.update_buffer_contents(ctx, quad_ibo_, 0, tcb::as_bytes(tcb::span(kIndices))); - quad_ibo_needs_upload_ = false; - } - - upload_screen(rhi, ctx); - upload_palette(rhi, ctx); - - // Calculate aspect ratio for black borders - float aspect = static_cast(vid.width) / static_cast(vid.height); - float real_aspect = static_cast(vid.realwidth) / static_cast(vid.realheight); - bool taller = aspect > real_aspect; - - std::array g1_uniforms = {{ - // Projection - std::array, 4> {{ - {taller ? 1.f : 1.f / real_aspect, 0.f, 0.f, 0.f}, - {0.f, taller ? -1.f / (1.f / real_aspect) : -1.f, 0.f, 0.f}, - {0.f, 0.f, 1.f, 0.f}, - {0.f, 0.f, 0.f, 1.f} - }}, - }}; - - std::array g2_uniforms = - {{ - // ModelView - std::array, 4> - {{ - {taller ? 2.f : 2.f * aspect, 0.f, 0.f, 0.f}, - {0.f, taller ? 2.f * (1.f / aspect) : 2.f, 0.f, 0.f}, - {0.f, 0.f, 1.f, 0.f}, - {0.f, 0.f, 0.f, 1.f} - }}, - // Texcoord0 Transform - std::array, 3> - {{ - {vid.width / static_cast(screen_tex_width_), 0.f, 0.f}, - {0.f, vid.height / static_cast(screen_tex_height_), 0.f}, - {0.f, 0.f, 1.f} - }} - }}; - - uniform_sets_[0] = rhi.create_uniform_set(ctx, {g1_uniforms}); - uniform_sets_[1] = rhi.create_uniform_set(ctx, {g2_uniforms}); - - std::array vbs = {{{0, quad_vbo_}}}; - std::array tbs = {{ - {rhi::SamplerName::kSampler0, screen_tex_}, - {rhi::SamplerName::kSampler1, palette_tex_} - }}; - binding_set_ = rhi.create_binding_set(ctx, pipeline_, {vbs, tbs}); + rhi.update_texture(ctx, screen_texture_, {0, 0, width_, height_}, PixelFormat::kR8, screen_span); } -void SoftwareBlitPass::graphics(Rhi& rhi, Handle ctx) +void SoftwarePass::graphics(Rhi& rhi, Handle ctx) { - rhi.begin_default_render_pass(ctx, true); - rhi.bind_pipeline(ctx, pipeline_); - rhi.bind_uniform_set(ctx, 0, uniform_sets_[0]); - rhi.bind_uniform_set(ctx, 1, uniform_sets_[1]); - rhi.bind_binding_set(ctx, binding_set_); - rhi.bind_index_buffer(ctx, quad_ibo_); - rhi.draw_indexed(ctx, 6, 0); - rhi.end_render_pass(ctx); } -void SoftwareBlitPass::postpass(Rhi& rhi) +void SoftwarePass::postpass(Rhi& rhi) { - // no-op } diff --git a/src/hwr2/pass_software.hpp b/src/hwr2/pass_software.hpp index f36c82973..4e7b02405 100644 --- a/src/hwr2/pass_software.hpp +++ b/src/hwr2/pass_software.hpp @@ -1,44 +1,46 @@ -#ifndef __SRB2_HWR2_PASS_SOFTWARE_HPP__ -#define __SRB2_HWR2_PASS_SOFTWARE_HPP__ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 +#ifndef __SRB2_HWR2_PASS_SOFTWARE_HPP_ +#define __SRB2_HWR2_PASS_SOFTWARE_HPP_ + +#include +#include -#include "../rhi/rhi.hpp" #include "pass.hpp" namespace srb2::hwr2 { -class SoftwareBlitPass : public Pass +/// @brief Renders software player views in prepass and uploads the result to a texture in transfer. +class SoftwarePass final : public Pass { - rhi::Handle pipeline_; - rhi::Handle screen_tex_; - rhi::Handle palette_tex_; - rhi::Handle quad_vbo_; - rhi::Handle quad_ibo_; - std::array, 2> uniform_sets_; - rhi::Handle binding_set_; + rhi::Handle screen_texture_; + uint32_t width_ = 0; + uint32_t height_ = 0; - uint32_t screen_tex_width_ = 0; - uint32_t screen_tex_height_ = 0; - bool quad_vbo_needs_upload_ = false; - bool quad_ibo_needs_upload_ = false; - - void upload_screen(rhi::Rhi& rhi, rhi::Handle ctx); - void upload_palette(rhi::Rhi& rhi, rhi::Handle ctx); + // Used to ensure the row spans are aligned on the unpack boundary for weird resolutions + // Any resolution with a width divisible by 4 doesn't need this, but e.g. 1366x768 needs the intermediary copy + std::vector copy_buffer_; public: - virtual ~SoftwareBlitPass(); + SoftwarePass(); + virtual ~SoftwarePass(); virtual void prepass(rhi::Rhi& rhi) override; - virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; - virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; - virtual void postpass(rhi::Rhi& rhi) override; + + rhi::Handle screen_texture() const noexcept { return screen_texture_; } }; } // namespace srb2::hwr2 -#endif // __SRB2_HWR2_PASS_SOFTWARE_HPP__ +#endif // __SRB2_HWR2_PASS_SOFTWARE_HPP_ diff --git a/src/hwr2/pass_twodee.cpp b/src/hwr2/pass_twodee.cpp new file mode 100644 index 000000000..e9316502f --- /dev/null +++ b/src/hwr2/pass_twodee.cpp @@ -0,0 +1,945 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "pass_twodee.hpp" + +#include + +#include + +#include "../r_patch.h" +#include "../v_video.h" +#include "../z_zone.h" + +using namespace srb2; +using namespace srb2::hwr2; +using namespace srb2::rhi; + +namespace +{ + +struct AtlasEntry +{ + uint32_t x; + uint32_t y; + uint32_t w; + uint32_t h; + + uint32_t trim_x; + uint32_t trim_y; + uint32_t orig_w; + uint32_t orig_h; +}; + +struct Atlas +{ + Atlas() = default; + Atlas(Atlas&&) = default; + + Handle tex; + uint32_t tex_width; + uint32_t tex_height; + std::unordered_map entries; + + std::unique_ptr rp_ctx {nullptr}; + std::unique_ptr rp_nodes {nullptr}; + + Atlas& operator=(Atlas&&) = default; +}; + +} // namespace + +struct srb2::hwr2::TwodeePassData +{ + Handle default_tex; + Handle default_colormap_tex; + std::vector patch_atlases; + std::unordered_map patch_lookup; + std::vector patches_to_upload; + std::unordered_map> colormaps; + std::vector colormaps_to_upload; + std::unordered_map> pipelines; + bool upload_default_tex = false; +}; + +std::shared_ptr srb2::hwr2::make_twodee_pass_data() +{ + return std::make_shared(); +} + +TwodeePass::TwodeePass() : Pass() +{ +} + +TwodeePass::~TwodeePass() = default; + +static constexpr const uint32_t kVboInitSize = 32768; +static constexpr const uint32_t kIboInitSize = 4096; + +static Rect trimmed_patch_dim(const patch_t* patch); + +static void create_atlas(Rhi& rhi, TwodeePassData& pass_data) +{ + Atlas new_atlas; + new_atlas.tex = rhi.create_texture({TextureFormat::kLuminanceAlpha, 2048, 2048}); + new_atlas.tex_width = 2048; + new_atlas.tex_height = 2048; + new_atlas.rp_ctx = std::make_unique(); + new_atlas.rp_nodes = std::make_unique(4096); + for (size_t i = 0; i < 4096; i++) + { + new_atlas.rp_nodes[i] = {}; + } + stbrp_init_target(new_atlas.rp_ctx.get(), 2048, 2048, new_atlas.rp_nodes.get(), 4096); + // it is CRITICALLY important that the atlas is MOVED, not COPIED, otherwise the node ptrs will be broken + pass_data.patch_atlases.push_back(std::move(new_atlas)); +} + +static void pack_patches(Rhi& rhi, TwodeePassData& pass_data, tcb::span patches) +{ + // Prepare stbrp rects for patches to be loaded. + std::vector rects; + for (size_t i = 0; i < patches.size(); i++) + { + const patch_t* patch = patches[i]; + Rect trimmed_rect = trimmed_patch_dim(patch); + stbrp_rect rect {}; + rect.id = i; + rect.w = trimmed_rect.w; + rect.h = trimmed_rect.h; + rects.push_back(std::move(rect)); + } + + while (rects.size() > 0) + { + if (pass_data.patch_atlases.size() == 0) + { + create_atlas(rhi, pass_data); + } + + for (size_t atlas_index = 0; atlas_index < pass_data.patch_atlases.size(); atlas_index++) + { + auto& atlas = pass_data.patch_atlases[atlas_index]; + + stbrp_pack_rects(atlas.rp_ctx.get(), rects.data(), rects.size()); + for (auto itr = rects.begin(); itr != rects.end();) + { + auto& rect = *itr; + if (rect.was_packed) + { + AtlasEntry entry; + const patch_t* patch = patches[rect.id]; + // TODO prevent unnecessary recalculation of trim? + Rect trimmed_rect = trimmed_patch_dim(patch); + entry.x = static_cast(rect.x); + entry.y = static_cast(rect.y); + entry.w = static_cast(rect.w); + entry.h = static_cast(rect.h); + entry.trim_x = static_cast(trimmed_rect.x); + entry.trim_y = static_cast(trimmed_rect.y); + entry.orig_w = static_cast(patch->width); + entry.orig_h = static_cast(patch->height); + atlas.entries.insert_or_assign(patch, std::move(entry)); + pass_data.patch_lookup.insert_or_assign(patch, atlas_index); + pass_data.patches_to_upload.push_back(patch); + rects.erase(itr); + continue; + } + ++itr; + } + + // If we still have rects to pack, and we're at the last atlas, create another atlas. + // TODO This could end up in an infinite loop if the patches are bigger than an atlas. Such patches need to + // be loaded as individual RHI textures instead. + if (atlas_index == pass_data.patch_atlases.size() - 1 && rects.size() > 0) + { + create_atlas(rhi, pass_data); + } + } + } +} + +/// @brief Derive the subrect of the given patch with empty columns and rows excluded. +static Rect trimmed_patch_dim(const patch_t* patch) +{ + bool minx_found = false; + int32_t minx = 0; + int32_t maxx = 0; + int32_t miny = patch->height; + int32_t maxy = 0; + for (int32_t x = 0; x < patch->width; x++) + { + const int32_t columnofs = patch->columnofs[x]; + const column_t* column = reinterpret_cast(patch->columns + columnofs); + + // If the first pole is empty (topdelta = 255), there are no pixels in this column + if (!minx_found && column->topdelta == 0xFF) + { + // Thus, the minx is at least one higher than the current column. + minx = x + 1; + continue; + } + minx_found = true; + + if (minx_found && column->topdelta != 0xFF) + { + maxx = x; + } + + miny = std::min(static_cast(column->topdelta), miny); + + int32_t prevdelta = 0; + int32_t topdelta = 0; + while (column->topdelta != 0xFF) + { + topdelta = column->topdelta; + + // Tall patches hack + if (topdelta <= prevdelta) + { + topdelta += prevdelta; + } + prevdelta = topdelta; + + maxy = std::max(topdelta + column->length, maxy); + + column = reinterpret_cast(reinterpret_cast(column) + column->length + 4); + } + } + + maxx += 1; + maxx = std::max(minx, maxx); + maxy = std::max(miny, maxy); + + return {minx, miny, static_cast(maxx - minx), static_cast(maxy - miny)}; +} + +static void convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector& out) +{ + Rect trimmed_rect = trimmed_patch_dim(patch); + if (trimmed_rect.w % 2 > 0) + { + // In order to force 4-byte row alignment, an extra column is added to the image data. + // Look up GL_UNPACK_ALIGNMENT (which defaults to 4 bytes) + trimmed_rect.w += 1; + } + out.clear(); + // 2 bytes per pixel; 1 for the color index, 1 for the alpha. (RG8) + out.resize(trimmed_rect.w * trimmed_rect.h * 2, 0); + for (int32_t x = 0; x < static_cast(trimmed_rect.w) && x < (patch->width - trimmed_rect.x); x++) + { + const int32_t columnofs = patch->columnofs[x + trimmed_rect.x]; + const column_t* column = reinterpret_cast(patch->columns + columnofs); + + int32_t prevdelta = 0; + int32_t topdelta = 0; + while (column->topdelta != 0xFF) + { + topdelta = column->topdelta; + // prevdelta is used to implement tall patches hack + if (topdelta <= prevdelta) + { + topdelta += prevdelta; + } + + prevdelta = topdelta; + const uint8_t* source = reinterpret_cast(column) + 3; + int32_t count = column->length; // is this byte order safe...? + + for (int32_t i = 0; i < count; i++) + { + int32_t output_y = topdelta + i - trimmed_rect.y; + if (output_y < 0) + { + continue; + } + if (output_y >= static_cast(trimmed_rect.h)) + { + break; + } + size_t pixel_index = (output_y * trimmed_rect.w + x) * 2; + out[pixel_index + 0] = source[i]; // index in luminance/red channel + out[pixel_index + 1] = 0xFF; // alpha/green value of 1 + } + column = reinterpret_cast(reinterpret_cast(column) + column->length + 4); + } + } +} + +static TwodeePipelineKey pipeline_key_for_cmd(const Draw2dCmd& cmd) +{ + return {hwr2::get_blend_mode(cmd), hwr2::is_draw_lines(cmd)}; +} + +static PipelineDesc make_pipeline_desc(TwodeePipelineKey key) +{ + constexpr const VertexInputDesc kTwodeeVertexInput = { + {{sizeof(TwodeeVertex)}}, + {{VertexAttributeName::kPosition, 0, 0}, + {VertexAttributeName::kTexCoord0, 0, 12}, + {VertexAttributeName::kColor, 0, 20}}}; + BlendDesc blend_desc; + switch (key.blend) + { + case Draw2dBlend::kAlphaTransparent: + blend_desc.source_factor_color = BlendFactor::kSourceAlpha; + blend_desc.dest_factor_color = BlendFactor::kOneMinusSourceAlpha; + blend_desc.color_function = BlendFunction::kAdd; + blend_desc.source_factor_alpha = BlendFactor::kOne; + blend_desc.dest_factor_alpha = BlendFactor::kOneMinusSourceAlpha; + blend_desc.alpha_function = BlendFunction::kAdd; + break; + case Draw2dBlend::kModulate: + blend_desc.source_factor_color = BlendFactor::kDest; + blend_desc.dest_factor_color = BlendFactor::kZero; + blend_desc.color_function = BlendFunction::kAdd; + blend_desc.source_factor_alpha = BlendFactor::kDestAlpha; + blend_desc.dest_factor_alpha = BlendFactor::kZero; + blend_desc.alpha_function = BlendFunction::kAdd; + break; + case Draw2dBlend::kAdditive: + blend_desc.source_factor_color = BlendFactor::kSourceAlpha; + blend_desc.dest_factor_color = BlendFactor::kOne; + blend_desc.color_function = BlendFunction::kAdd; + blend_desc.source_factor_alpha = BlendFactor::kOne; + blend_desc.dest_factor_alpha = BlendFactor::kOneMinusSourceAlpha; + blend_desc.alpha_function = BlendFunction::kAdd; + break; + case Draw2dBlend::kSubtractive: + blend_desc.source_factor_color = BlendFactor::kSourceAlpha; + blend_desc.dest_factor_color = BlendFactor::kOne; + blend_desc.color_function = BlendFunction::kSubtract; + blend_desc.source_factor_alpha = BlendFactor::kOne; + blend_desc.dest_factor_alpha = BlendFactor::kOneMinusSourceAlpha; + blend_desc.alpha_function = BlendFunction::kAdd; + break; + case Draw2dBlend::kReverseSubtractive: + blend_desc.source_factor_color = BlendFactor::kSourceAlpha; + blend_desc.dest_factor_color = BlendFactor::kOne; + blend_desc.color_function = BlendFunction::kReverseSubtract; + blend_desc.source_factor_alpha = BlendFactor::kOne; + blend_desc.dest_factor_alpha = BlendFactor::kOneMinusSourceAlpha; + blend_desc.alpha_function = BlendFunction::kAdd; + break; + case Draw2dBlend::kInvertDest: + blend_desc.source_factor_color = BlendFactor::kOne; + blend_desc.dest_factor_color = BlendFactor::kOne; + blend_desc.color_function = BlendFunction::kSubtract; + blend_desc.source_factor_alpha = BlendFactor::kZero; + blend_desc.dest_factor_alpha = BlendFactor::kDestAlpha; + blend_desc.alpha_function = BlendFunction::kAdd; + break; + } + + return { + PipelineProgram::kUnshadedPaletted, + kTwodeeVertexInput, + {{{{UniformName::kProjection}}, + {{UniformName::kModelView, UniformName::kTexCoord0Transform, UniformName::kSampler0IsIndexedAlpha}}}}, + {{SamplerName::kSampler0, SamplerName::kSampler1, SamplerName::kSampler2}}, + std::nullopt, + {PixelFormat::kRGBA8, blend_desc, {true, true, true, true}}, + key.lines ? PrimitiveType::kLines : PrimitiveType::kTriangles, + CullMode::kNone, + FaceWinding::kCounterClockwise, + {0.f, 0.f, 0.f, 1.f}}; +} + +static void rewrite_patch_quad_vertices(Draw2dList& list, const Draw2dPatchQuad& cmd, TwodeePassData* data) +{ + // Patch quads are clipped according to the patch's atlas entry + if (cmd.patch == nullptr) + { + return; + } + + std::size_t atlas_index = data->patch_lookup[cmd.patch]; + auto& atlas = data->patch_atlases[atlas_index]; + auto& entry = atlas.entries[cmd.patch]; + + // Rewrite the vertex data completely. + // The UVs of the trimmed patch in atlas UV space. + const float atlas_umin = static_cast(entry.x) / atlas.tex_width; + const float atlas_umax = static_cast(entry.x + entry.w) / atlas.tex_width; + const float atlas_vmin = static_cast(entry.y) / atlas.tex_height; + const float atlas_vmax = static_cast(entry.y + entry.h) / atlas.tex_height; + + // The UVs of the trimmed patch in untrimmed UV space. + // The command's UVs are in untrimmed UV space. + const float trim_umin = static_cast(entry.trim_x) / entry.orig_w; + const float trim_umax = static_cast(entry.trim_x + entry.w) / entry.orig_w; + const float trim_vmin = static_cast(entry.trim_y) / entry.orig_h; + const float trim_vmax = static_cast(entry.trim_y + entry.h) / entry.orig_h; + + // Calculate positions + const float cmd_xrange = cmd.xmax - cmd.xmin; + const float cmd_yrange = cmd.ymax - cmd.ymin; + const float clipped_xmin = cmd.clip ? std::clamp(cmd.xmin, cmd.clip_xmin, cmd.clip_xmax) : cmd.xmin; + const float clipped_xmax = cmd.clip ? std::clamp(cmd.xmax, cmd.clip_xmin, cmd.clip_xmax) : cmd.xmax; + const float clipped_ymin = cmd.clip ? std::clamp(cmd.ymin, cmd.clip_ymin, cmd.clip_ymax) : cmd.ymin; + const float clipped_ymax = cmd.clip ? std::clamp(cmd.ymax, cmd.clip_ymin, cmd.clip_ymax) : cmd.ymax; + const float trimmed_xmin = cmd.xmin + trim_umin * cmd_xrange; + const float trimmed_xmax = cmd.xmax - (1.f - trim_umax) * cmd_xrange; + const float trimmed_ymin = cmd.ymin + trim_vmin * cmd_yrange; + const float trimmed_ymax = cmd.ymax - (1.f - trim_vmax) * cmd_yrange; + const float trimmed_xrange = trimmed_xmax - trimmed_xmin; + const float trimmed_yrange = trimmed_ymax - trimmed_ymin; + float clipped_trimmed_xmin = std::max(clipped_xmin, trimmed_xmin); + float clipped_trimmed_xmax = std::min(clipped_xmax, trimmed_xmax); + float clipped_trimmed_ymin = std::max(clipped_ymin, trimmed_ymin); + float clipped_trimmed_ymax = std::min(clipped_ymax, trimmed_ymax); + clipped_trimmed_xmin = std::min(clipped_trimmed_xmin, clipped_trimmed_xmax); + clipped_trimmed_ymin = std::min(clipped_trimmed_ymin, clipped_trimmed_ymax); + + // Calculate UVs + // Start from trimmed dimensions as 0..1 and clip UVs based on that + // UVs in trimmed UV space (if clipped_xmin = trimmed_xmin, it'll be 0) + float clipped_umin; + float clipped_umax; + float clipped_vmin; + float clipped_vmax; + + if (cmd.flip) + { + clipped_umin = std::max(0.f, 1.f - (clipped_trimmed_xmin - trimmed_xmin) / trimmed_xrange); + clipped_umax = std::min(1.f, (trimmed_xmax - clipped_trimmed_xmax) / trimmed_xrange); + } + else + { + clipped_umin = std::min(1.f, (clipped_trimmed_xmin - trimmed_xmin) / trimmed_xrange); + clipped_umax = std::max(0.f, 1.f - (trimmed_xmax - clipped_trimmed_xmax) / trimmed_xrange); + } + + if (cmd.vflip) + { + clipped_vmin = std::max(0.f, 1.f - (clipped_trimmed_ymin - trimmed_ymin) / trimmed_yrange); + clipped_vmax = std::min(1.f, (trimmed_ymax - clipped_trimmed_ymax) / trimmed_yrange); + } + else + { + clipped_vmin = std::min(1.f, 0.f + (clipped_trimmed_ymin - trimmed_ymin) / trimmed_yrange); + clipped_vmax = std::max(0.f, 1.f - (trimmed_ymax - clipped_trimmed_ymax) / trimmed_yrange); + } + + // convert from trimmed UV space to atlas space + clipped_umin = (atlas_umax - atlas_umin) * clipped_umin + atlas_umin; + clipped_umax = (atlas_umax - atlas_umin) * clipped_umax + atlas_umin; + clipped_vmin = (atlas_vmax - atlas_vmin) * clipped_vmin + atlas_vmin; + clipped_vmax = (atlas_vmax - atlas_vmin) * clipped_vmax + atlas_vmin; + + std::size_t vtx_offs = cmd.begin_index; + // Vertex order is always min/min, max/min, max/max, min/max + list.vertices[vtx_offs + 0].x = clipped_trimmed_xmin; + list.vertices[vtx_offs + 0].y = clipped_trimmed_ymin; + list.vertices[vtx_offs + 0].u = clipped_umin; + list.vertices[vtx_offs + 0].v = clipped_vmin; + list.vertices[vtx_offs + 1].x = clipped_trimmed_xmax; + list.vertices[vtx_offs + 1].y = clipped_trimmed_ymin; + list.vertices[vtx_offs + 1].u = clipped_umax; + list.vertices[vtx_offs + 1].v = clipped_vmin; + list.vertices[vtx_offs + 2].x = clipped_trimmed_xmax; + list.vertices[vtx_offs + 2].y = clipped_trimmed_ymax; + list.vertices[vtx_offs + 2].u = clipped_umax; + list.vertices[vtx_offs + 2].v = clipped_vmax; + list.vertices[vtx_offs + 3].x = clipped_trimmed_xmin; + list.vertices[vtx_offs + 3].y = clipped_trimmed_ymax; + list.vertices[vtx_offs + 3].u = clipped_umin; + list.vertices[vtx_offs + 3].v = clipped_vmax; +} + +void TwodeePass::prepass(Rhi& rhi) +{ + if (!ctx_ || !data_) + { + return; + } + + if (data_->pipelines.size() == 0) + { + TwodeePipelineKey alpha_transparent_tris = {Draw2dBlend::kAlphaTransparent, false}; + TwodeePipelineKey modulate_tris = {Draw2dBlend::kModulate, false}; + TwodeePipelineKey additive_tris = {Draw2dBlend::kAdditive, false}; + TwodeePipelineKey subtractive_tris = {Draw2dBlend::kSubtractive, false}; + TwodeePipelineKey revsubtractive_tris = {Draw2dBlend::kReverseSubtractive, false}; + TwodeePipelineKey invertdest_tris = {Draw2dBlend::kInvertDest, false}; + TwodeePipelineKey alpha_transparent_lines = {Draw2dBlend::kAlphaTransparent, true}; + TwodeePipelineKey modulate_lines = {Draw2dBlend::kModulate, true}; + TwodeePipelineKey additive_lines = {Draw2dBlend::kAdditive, true}; + TwodeePipelineKey subtractive_lines = {Draw2dBlend::kSubtractive, true}; + TwodeePipelineKey revsubtractive_lines = {Draw2dBlend::kReverseSubtractive, true}; + TwodeePipelineKey invertdest_lines = {Draw2dBlend::kInvertDest, true}; + data_->pipelines.insert({alpha_transparent_tris, rhi.create_pipeline(make_pipeline_desc(alpha_transparent_tris))}); + data_->pipelines.insert({modulate_tris, rhi.create_pipeline(make_pipeline_desc(modulate_tris))}); + data_->pipelines.insert({additive_tris, rhi.create_pipeline(make_pipeline_desc(additive_tris))}); + data_->pipelines.insert({subtractive_tris, rhi.create_pipeline(make_pipeline_desc(subtractive_tris))}); + data_->pipelines.insert({revsubtractive_tris, rhi.create_pipeline(make_pipeline_desc(revsubtractive_tris))}); + data_->pipelines.insert({invertdest_tris, rhi.create_pipeline(make_pipeline_desc(invertdest_tris))}); + data_->pipelines.insert({alpha_transparent_lines, rhi.create_pipeline(make_pipeline_desc(alpha_transparent_lines))}); + data_->pipelines.insert({modulate_lines, rhi.create_pipeline(make_pipeline_desc(modulate_lines))}); + data_->pipelines.insert({additive_lines, rhi.create_pipeline(make_pipeline_desc(additive_lines))}); + data_->pipelines.insert({subtractive_lines, rhi.create_pipeline(make_pipeline_desc(subtractive_lines))}); + data_->pipelines.insert({revsubtractive_lines, rhi.create_pipeline(make_pipeline_desc(revsubtractive_lines))}); + data_->pipelines.insert({invertdest_lines, rhi.create_pipeline(make_pipeline_desc(revsubtractive_lines))}); + } + + if (!data_->default_tex) + { + data_->default_tex = rhi.create_texture({TextureFormat::kLuminanceAlpha, 2, 1}); + data_->upload_default_tex = true; + } + if (!data_->default_colormap_tex) + { + data_->default_colormap_tex = rhi.create_texture({TextureFormat::kLuminance, 256, 1}); + data_->upload_default_tex = true; + } + if (!render_pass_) + { + render_pass_ = rhi.create_render_pass( + {std::nullopt, PixelFormat::kRGBA8, AttachmentLoadOp::kLoad, AttachmentStoreOp::kStore} + ); + } + + // Check for patches that are being freed after this frame. Those patches must be present in the atlases for this + // frame, but all atlases need to be cleared and rebuilt on next call to prepass. + // This is based on the assumption that patches are very rarely freed during runtime; occasionally repacking the + // atlases to free up space from patches that will never be referenced again is acceptable. + if (rebuild_atlases_) + { + for (auto& atlas : data_->patch_atlases) + { + rhi.destroy_texture(atlas.tex); + } + data_->patch_atlases.clear(); + data_->patch_lookup.clear(); + rebuild_atlases_ = false; + } + + if (data_->patch_atlases.size() > 2) + { + // Rebuild the atlases next frame because we have too many patches in the atlas cache. + rebuild_atlases_ = true; + } + + // Stage 1 - command list patch detection + std::unordered_set found_patches; + std::unordered_set found_colormaps; + for (const auto& list : *ctx_) + { + for (const auto& cmd : list.cmds) + { + auto visitor = srb2::Overload { + [&](const Draw2dPatchQuad& cmd) + { + if (cmd.patch != nullptr) + { + found_patches.insert(cmd.patch); + } + if (cmd.colormap != nullptr) + { + found_colormaps.insert(cmd.colormap); + } + }, + [&](const Draw2dVertices& cmd) {}}; + std::visit(visitor, cmd); + } + } + + std::unordered_set patch_cache_hits; + std::unordered_set patch_cache_misses; + for (auto patch : found_patches) + { + if (data_->patch_lookup.find(patch) != data_->patch_lookup.end()) + { + patch_cache_hits.insert(patch); + } + else + { + patch_cache_misses.insert(patch); + } + } + + for (auto colormap : found_colormaps) + { + if (data_->colormaps.find(colormap) == data_->colormaps.end()) + { + Handle colormap_tex = rhi.create_texture({TextureFormat::kLuminance, 256, 1}); + data_->colormaps.insert({colormap, colormap_tex}); + } + + data_->colormaps_to_upload.push_back(colormap); + } + + // Stage 2 - pack rects into atlases + std::vector patches_to_pack(patch_cache_misses.begin(), patch_cache_misses.end()); + pack_patches(rhi, *data_, patches_to_pack); + // We now know what patches need to be uploaded. + + size_t list_index = 0; + for (auto& list : *ctx_) + { + Handle vbo; + uint32_t vertex_data_size = tcb::as_bytes(tcb::span(list.vertices)).size(); + uint32_t needed_vbo_size = std::max( + kVboInitSize, + ((static_cast(vertex_data_size) + kVboInitSize - 1) / kVboInitSize) * kVboInitSize + ); + + // Get the existing buffer objects. Recreate them if they don't exist, or needs to be bigger. + + if (list_index >= vbos_.size()) + { + vbo = rhi.create_buffer({needed_vbo_size, BufferType::kVertexBuffer, BufferUsage::kDynamic}); + vbos_.push_back({vbo, needed_vbo_size}); + } + else + { + uint32_t existing_size = std::get<1>(vbos_[list_index]); + if (needed_vbo_size > existing_size) + { + rhi.destroy_buffer(std::get<0>(vbos_[list_index])); + vbo = rhi.create_buffer({needed_vbo_size, BufferType::kVertexBuffer, BufferUsage::kDynamic}); + vbos_[list_index] = {vbo, needed_vbo_size}; + } + vbo = std::get<0>(vbos_[list_index]); + } + + Handle ibo; + uint32_t index_data_size = tcb::as_bytes(tcb::span(list.indices)).size(); + uint32_t needed_ibo_size = std::max( + kIboInitSize, + ((static_cast(index_data_size) + kIboInitSize - 1) / kIboInitSize) * kIboInitSize + ); + + if (list_index >= ibos_.size()) + { + ibo = rhi.create_buffer({needed_ibo_size, BufferType::kIndexBuffer, BufferUsage::kDynamic}); + ibos_.push_back({ibo, needed_ibo_size}); + } + else + { + uint32_t existing_size = std::get<1>(ibos_[list_index]); + if (needed_ibo_size > existing_size) + { + rhi.destroy_buffer(std::get<0>(ibos_[list_index])); + ibo = rhi.create_buffer({needed_ibo_size, BufferType::kIndexBuffer, BufferUsage::kDynamic}); + ibos_[list_index] = {ibo, needed_ibo_size}; + } + ibo = std::get<0>(ibos_[list_index]); + } + + // Create a merged command list + MergedTwodeeCommandList merged_list; + merged_list.vbo = vbo; + merged_list.vbo_size = needed_vbo_size; + merged_list.ibo = ibo; + merged_list.ibo_size = needed_ibo_size; + + MergedTwodeeCommand new_cmd; + new_cmd.index_offset = 0; + new_cmd.elements = 0; + new_cmd.colormap = nullptr; + // safety: a command list is required to have at least 1 command + new_cmd.pipeline_key = pipeline_key_for_cmd(list.cmds[0]); + merged_list.cmds.push_back(std::move(new_cmd)); + + for (auto& cmd : list.cmds) + { + auto& merged_cmd = *merged_list.cmds.rbegin(); + bool new_cmd_needed = false; + TwodeePipelineKey pk = pipeline_key_for_cmd(cmd); + new_cmd_needed = new_cmd_needed || (pk != merged_cmd.pipeline_key); + + // We need to split the merged commands based on the kind of texture + // Patches are converted to atlas texture indexes, which we've just packed the patch rects for + // Flats are uploaded as individual textures. + // TODO actually implement flat drawing + auto tex_visitor = srb2::Overload { + [&](const Draw2dPatchQuad& cmd) + { + if (cmd.patch == nullptr) + { + new_cmd_needed = new_cmd_needed || (merged_cmd.texture != std::nullopt); + } + else + { + size_t atlas_index = data_->patch_lookup[cmd.patch]; + typeof(merged_cmd.texture) atlas_index_texture = atlas_index; + new_cmd_needed = new_cmd_needed || (merged_cmd.texture != atlas_index_texture); + } + + new_cmd_needed = new_cmd_needed || (merged_cmd.colormap != cmd.colormap); + }, + [&](const Draw2dVertices& cmd) + { + if (cmd.flat_lump == LUMPERROR) + { + new_cmd_needed |= (merged_cmd.texture != std::nullopt); + } + else + { + typeof(merged_cmd.texture) flat_tex = MergedTwodeeCommandFlatTexture {cmd.flat_lump}; + new_cmd_needed |= (merged_cmd.texture != flat_tex); + } + + new_cmd_needed = new_cmd_needed || (merged_cmd.colormap != nullptr); + }}; + std::visit(tex_visitor, cmd); + + if (new_cmd_needed) + { + MergedTwodeeCommand the_new_one; + the_new_one.index_offset = merged_cmd.index_offset + merged_cmd.elements; + + // Map to the merged version of the texture variant. Yay...! + auto tex_visitor_again = srb2::Overload { + [&](const Draw2dPatchQuad& cmd) + { + if (cmd.patch != nullptr) + { + the_new_one.texture = data_->patch_lookup[cmd.patch]; + } + else + { + the_new_one.texture = std::nullopt; + } + the_new_one.colormap = cmd.colormap; + }, + [&](const Draw2dVertices& cmd) + { + if (cmd.flat_lump != LUMPERROR) + { + flat_manager_->find_or_create_indexed(rhi, cmd.flat_lump); + typeof(the_new_one.texture) t = MergedTwodeeCommandFlatTexture {cmd.flat_lump}; + the_new_one.texture = t; + } + else + { + the_new_one.texture = std::nullopt; + } + + the_new_one.colormap = nullptr; + }}; + std::visit(tex_visitor_again, cmd); + the_new_one.pipeline_key = pipeline_key_for_cmd(cmd); + merged_list.cmds.push_back(std::move(the_new_one)); + } + + // There may or may not be a new current command; update its element count + auto& new_merged_cmd = *merged_list.cmds.rbegin(); + // We know for sure that all commands in a command list have a contiguous range of elements in the IBO + // So we can draw them in batch if the pipeline key and textures match + new_merged_cmd.elements += hwr2::elements(cmd); + + // Perform coordinate transformations + { + auto vtx_transform_visitor = srb2::Overload { + [&](const Draw2dPatchQuad& cmd) { rewrite_patch_quad_vertices(list, cmd, data_.get()); }, + [&](const Draw2dVertices& cmd) {}}; + std::visit(vtx_transform_visitor, cmd); + } + } + + cmd_lists_.push_back(std::move(merged_list)); + + list_index++; + } +} + +void TwodeePass::transfer(Rhi& rhi, Handle ctx) +{ + if (!ctx_ || !data_) + { + return; + } + + if (data_->upload_default_tex) + { + std::array data = {0, 255, 0, 255}; + rhi.update_texture(ctx, data_->default_tex, {0, 0, 2, 1}, PixelFormat::kRG8, tcb::as_bytes(tcb::span(data))); + + std::array colormap_data; + for (size_t i = 0; i < 256; i++) + { + colormap_data[i] = i; + } + rhi.update_texture( + ctx, + data_->default_colormap_tex, + {0, 0, 256, 1}, + PixelFormat::kR8, + tcb::as_bytes(tcb::span(colormap_data)) + ); + + data_->upload_default_tex = false; + } + + for (auto colormap : data_->colormaps_to_upload) + { + rhi.update_texture( + ctx, + data_->colormaps[colormap], + {0, 0, 256, 1}, + rhi::PixelFormat::kR8, + tcb::as_bytes(tcb::span(colormap, 256)) + ); + } + data_->colormaps_to_upload.clear(); + + // Convert patches to RG8 textures and upload to atlas pages + std::vector patch_data; + for (const patch_t* patch_to_upload : data_->patches_to_upload) + { + Atlas& atlas = data_->patch_atlases[data_->patch_lookup[patch_to_upload]]; + AtlasEntry& entry = atlas.entries[patch_to_upload]; + + convert_patch_to_trimmed_rg8_pixels(patch_to_upload, patch_data); + + rhi.update_texture( + ctx, + atlas.tex, + {static_cast(entry.x), static_cast(entry.y), entry.w, entry.h}, + PixelFormat::kRG8, + tcb::as_bytes(tcb::span(patch_data)) + ); + } + data_->patches_to_upload.clear(); + + Handle palette_tex = palette_manager_->palette(); + + // Update the buffers for each list + auto ctx_list_itr = ctx_->begin(); + for (size_t i = 0; i < cmd_lists_.size() && ctx_list_itr != ctx_->end(); i++) + { + auto& merged_list = cmd_lists_[i]; + auto& orig_list = *ctx_list_itr; + + tcb::span vertex_data = tcb::as_bytes(tcb::span(orig_list.vertices)); + tcb::span index_data = tcb::as_bytes(tcb::span(orig_list.indices)); + rhi.update_buffer_contents(ctx, merged_list.vbo, 0, vertex_data); + rhi.update_buffer_contents(ctx, merged_list.ibo, 0, index_data); + + // Update the binding sets for each individual merged command + VertexAttributeBufferBinding vbos[] = {{0, merged_list.vbo}}; + for (auto& mcmd : merged_list.cmds) + { + TextureBinding tx[3]; + auto tex_visitor = srb2::Overload { + [&](size_t atlas_index) + { + Atlas& atlas = data_->patch_atlases[atlas_index]; + tx[0] = {SamplerName::kSampler0, atlas.tex}; + tx[1] = {SamplerName::kSampler1, palette_tex}; + }, + [&](const MergedTwodeeCommandFlatTexture& tex) + { + Handle th = flat_manager_->find_indexed(tex.lump); + SRB2_ASSERT(th != kNullHandle); + tx[0] = {SamplerName::kSampler0, th}; + tx[1] = {SamplerName::kSampler1, palette_tex}; + }}; + if (mcmd.texture) + { + std::visit(tex_visitor, *mcmd.texture); + } + else + { + tx[0] = {SamplerName::kSampler0, data_->default_tex}; + tx[1] = {SamplerName::kSampler1, palette_tex}; + } + + const uint8_t* colormap = mcmd.colormap; + Handle colormap_h = data_->default_colormap_tex; + if (colormap) + { + SRB2_ASSERT(data_->colormaps.find(colormap) != data_->colormaps.end()); + colormap_h = data_->colormaps[colormap]; + } + tx[2] = {SamplerName::kSampler2, colormap_h}; + mcmd.binding_set = + rhi.create_binding_set(ctx, data_->pipelines[mcmd.pipeline_key], {tcb::span(vbos), tcb::span(tx)}); + } + + ctx_list_itr++; + } + + // Uniform sets + std::array g1_uniforms = {{ + // Projection + std::array, 4> { + {{2.f / vid.width, 0.f, 0.f, 0.f}, + {0.f, -2.f / vid.height, 0.f, 0.f}, + {0.f, 0.f, 1.f, 0.f}, + {-1.f, 1.f, 0.f, 1.f}}}, + }}; + std::array g2_uniforms = { + {// ModelView + std::array, 4> { + {{1.f, 0.f, 0.f, 0.f}, {0.f, 1.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}, {0.f, 0.f, 0.f, 1.f}}}, + // Texcoord0 Transform + std::array, 3> {{{1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}}}, + // Sampler 0 Is Indexed Alpha (yes, it always is) + static_cast(1)}}; + us_1 = rhi.create_uniform_set(ctx, {tcb::span(g1_uniforms)}); + us_2 = rhi.create_uniform_set(ctx, {tcb::span(g2_uniforms)}); +} + +static constexpr const rhi::Color kClearColor = {0, 0, 0, 1}; + +void TwodeePass::graphics(Rhi& rhi, Handle ctx) +{ + if (!ctx_ || !data_) + { + return; + } + + if (output_) + { + rhi.begin_render_pass(ctx, {render_pass_, output_, std::nullopt, kClearColor}); + } + else + { + rhi.begin_default_render_pass(ctx, false); + } + + for (auto& list : cmd_lists_) + { + for (auto& cmd : list.cmds) + { + if (cmd.elements == 0) + { + // Don't do anything for 0-element commands + // This shouldn't happen, but, just in case... + continue; + } + SRB2_ASSERT(data_->pipelines.find(cmd.pipeline_key) != data_->pipelines.end()); + Handle pl = data_->pipelines[cmd.pipeline_key]; + rhi.bind_pipeline(ctx, pl); + if (output_) + { + rhi.set_viewport(ctx, {0, 0, output_width_, output_height_}); + } + rhi.bind_uniform_set(ctx, 0, us_1); + rhi.bind_uniform_set(ctx, 1, us_2); + rhi.bind_binding_set(ctx, cmd.binding_set); + rhi.bind_index_buffer(ctx, list.ibo); + rhi.draw_indexed(ctx, cmd.elements, cmd.index_offset); + } + } + rhi.end_render_pass(ctx); +} + +void TwodeePass::postpass(Rhi& rhi) +{ + if (!ctx_ || !data_) + { + return; + } + + cmd_lists_.clear(); +} diff --git a/src/hwr2/pass_twodee.hpp b/src/hwr2/pass_twodee.hpp new file mode 100644 index 000000000..4edb495c2 --- /dev/null +++ b/src/hwr2/pass_twodee.hpp @@ -0,0 +1,117 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_PASS_TWODEE_HPP__ +#define __SRB2_HWR2_PASS_TWODEE_HPP__ + +#include +#include +#include +#include +#include +#include + +#include "../cxxutil.hpp" +#include "pass.hpp" +#include "pass_resource_managers.hpp" +#include "twodee.hpp" + +namespace srb2::hwr2 +{ + +class TwodeePass; + +/// @brief Shared structures to allow multiple 2D instances to share the same atlases +struct TwodeePassData; + +/// @brief Hash map key for caching pipelines +struct TwodeePipelineKey +{ + Draw2dBlend blend; + bool lines; + + bool operator==(const TwodeePipelineKey& r) const noexcept { return !(blend != r.blend || lines != r.lines); } + bool operator!=(const TwodeePipelineKey& r) const noexcept { return !(*this == r); } +}; + +struct MergedTwodeeCommandFlatTexture +{ + lumpnum_t lump; + + bool operator==(const MergedTwodeeCommandFlatTexture& rhs) const noexcept { return lump == rhs.lump; } + bool operator!=(const MergedTwodeeCommandFlatTexture& rhs) const noexcept { return !(*this == rhs); } +}; + +struct MergedTwodeeCommand +{ + TwodeePipelineKey pipeline_key = {}; + rhi::Handle binding_set = {}; + std::optional> texture; + const uint8_t* colormap; + uint32_t index_offset = 0; + uint32_t elements = 0; +}; + +struct MergedTwodeeCommandList +{ + rhi::Handle vbo {}; + uint32_t vbo_size = 0; + rhi::Handle ibo {}; + uint32_t ibo_size = 0; + + std::vector cmds; +}; + +std::shared_ptr make_twodee_pass_data(); + +struct TwodeePass final : public Pass +{ + Twodee* ctx_ = nullptr; + std::variant, rhi::Handle> out_color_; + + std::shared_ptr data_; + std::shared_ptr palette_manager_; + std::shared_ptr flat_manager_; + rhi::Handle us_1; + rhi::Handle us_2; + std::vector cmd_lists_; + std::vector, std::size_t>> vbos_; + std::vector, std::size_t>> ibos_; + bool rebuild_atlases_ = false; + rhi::Handle render_pass_; + rhi::Handle output_; + uint32_t output_width_ = 0; + uint32_t output_height_ = 0; + + TwodeePass(); + virtual ~TwodeePass(); + + virtual void prepass(rhi::Rhi& rhi) override; + + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + + virtual void postpass(rhi::Rhi& rhi) override; +}; + +} // namespace srb2::hwr2 + +template <> +struct std::hash +{ + std::size_t operator()(const srb2::hwr2::TwodeePipelineKey& v) const + { + std::size_t hash = 0; + srb2::hash_combine(hash, v.blend, v.lines); + return hash; + } +}; + +#endif // __SRB2_HWR2_PASS_TWODEE_HPP__ diff --git a/src/hwr2/twodee.cpp b/src/hwr2/twodee.cpp new file mode 100644 index 000000000..8a15234fe --- /dev/null +++ b/src/hwr2/twodee.cpp @@ -0,0 +1,114 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "twodee.hpp" + +#include "../w_wad.h" + +using namespace srb2; +using namespace hwr2; + +Twodee::Twodee() = default; +Twodee::Twodee(const Twodee&) = default; +Twodee::Twodee(Twodee&&) noexcept = default; +Twodee& Twodee::operator=(const Twodee&) = default; + +// Will the default move prevent the vectors from losing their allocations? I guess it depends on the STL impl. +// It's probably worth optimizing around. +Twodee& Twodee::operator=(Twodee&&) noexcept = default; + +void Draw2dQuadBuilder::done() +{ + if (ctx_.lists_.size() == 0) + { + ctx_.lists_.push_back({}); + } + + if (ctx_.lists_.rbegin()->vertices.size() >= (Draw2dList::kMaxVertices - 4)) + { + // The current draw list has too many vertices to fit this command + ctx_.lists_.push_back({}); + } + + auto& list = *ctx_.lists_.rbegin(); + quad_.begin_element = list.vertices.size(); + quad_.begin_index = list.vertices.size(); + + list.vertices.push_back({quad_.xmin, quad_.ymin, 0.f, 0, 0, quad_.r, quad_.g, quad_.b, quad_.a}); + list.vertices.push_back({quad_.xmax, quad_.ymin, 0.f, 1, 0, quad_.r, quad_.g, quad_.b, quad_.a}); + list.vertices.push_back({quad_.xmax, quad_.ymax, 0.f, 1, 1, quad_.r, quad_.g, quad_.b, quad_.a}); + list.vertices.push_back({quad_.xmin, quad_.ymax, 0.f, 0, 1, quad_.r, quad_.g, quad_.b, quad_.a}); + + list.indices.push_back(quad_.begin_element + 0); + list.indices.push_back(quad_.begin_element + 1); + list.indices.push_back(quad_.begin_element + 2); + + list.indices.push_back(quad_.begin_element + 0); + list.indices.push_back(quad_.begin_element + 2); + list.indices.push_back(quad_.begin_element + 3); + + list.cmds.push_back(quad_); +} + +void Draw2dVerticesBuilder::done() +{ + if (ctx_.lists_.size() == 0) + { + ctx_.lists_.push_back({}); + } + + if (ctx_.lists_.rbegin()->vertices.size() >= (Draw2dList::kMaxVertices - 4)) + { + // The current draw list has too many vertices to fit this command + ctx_.lists_.push_back({}); + } + + auto& list = *ctx_.lists_.rbegin(); + tris_.begin_element = list.vertices.size(); + tris_.begin_index = list.indices.size(); + + if (verts_.empty()) + { + return; + } + + std::size_t i = 0; + for (auto& vert : verts_) + { + list.vertices.push_back({vert[0], vert[1], 0, vert[2], vert[3], r_, g_, b_, a_}); + list.indices.push_back(tris_.begin_element + i); + i++; + } + + list.cmds.push_back(tris_); +} + +Draw2dBlend srb2::hwr2::get_blend_mode(const Draw2dCmd& cmd) noexcept +{ + auto visitor = srb2::Overload { + [&](const Draw2dPatchQuad& cmd) { return cmd.blend; }, + [&](const Draw2dVertices& cmd) { return cmd.blend; }}; + return std::visit(visitor, cmd); +} + +bool srb2::hwr2::is_draw_lines(const Draw2dCmd& cmd) noexcept +{ + auto visitor = srb2::Overload { + [&](const Draw2dPatchQuad& cmd) { return false; }, + [&](const Draw2dVertices& cmd) { return cmd.lines; }}; + return std::visit(visitor, cmd); +} + +std::size_t srb2::hwr2::elements(const Draw2dCmd& cmd) noexcept +{ + auto visitor = srb2::Overload { + [&](const Draw2dPatchQuad& cmd) -> std::size_t { return 6; }, + [&](const Draw2dVertices& cmd) -> std::size_t { return cmd.elements; }}; + return std::visit(visitor, cmd); +} diff --git a/src/hwr2/twodee.hpp b/src/hwr2/twodee.hpp new file mode 100644 index 000000000..af0f60bc5 --- /dev/null +++ b/src/hwr2/twodee.hpp @@ -0,0 +1,281 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_HWR2_TWODEE_HPP__ +#define __SRB2_HWR2_TWODEE_HPP__ + +#include +#include +#include +#include +#include +#include + +#include + +#include "../cxxutil.hpp" +#include "../doomtype.h" + +namespace srb2::hwr2 +{ + +struct TwodeeVertex +{ + float x; + float y; + float z; + float u; + float v; + float r; + float g; + float b; + float a; +}; + +enum class Draw2dBlend +{ + kAlphaTransparent, + kModulate, + kAdditive, + kSubtractive, + kReverseSubtractive, + kInvertDest +}; + +struct Draw2dPatchQuad +{ + std::size_t begin_index = 0; + std::size_t begin_element = 0; + + // A null patch ptr means no patch is drawn + const patch_t* patch = nullptr; + const uint8_t* colormap = nullptr; + Draw2dBlend blend; + float r = 0.f; + float g = 0.f; + float b = 0.f; + float a = 0.f; + + // Size fields are made available to let the consumer modify the vertex data for optimization + float xmin = 0.f; + float ymin = 0.f; + float xmax = 0.f; + float ymax = 0.f; + float clip_xmin = 0.f; + float clip_xmax = 0.f; + float clip_ymin = 0.f; + float clip_ymax = 0.f; + bool clip = false; + bool flip = false; + bool vflip = false; +}; + +struct Draw2dVertices +{ + std::size_t begin_index = 0; + std::size_t begin_element = 0; + std::size_t elements = 0; + Draw2dBlend blend = Draw2dBlend::kAlphaTransparent; + lumpnum_t flat_lump = UINT32_MAX; // LUMPERROR but not loading w_wad.h from this header + bool lines = false; +}; + +using Draw2dCmd = std::variant; + +Draw2dBlend get_blend_mode(const Draw2dCmd& cmd) noexcept; +bool is_draw_lines(const Draw2dCmd& cmd) noexcept; +std::size_t elements(const Draw2dCmd& cmd) noexcept; + +struct Draw2dList +{ + std::vector vertices; + std::vector indices; + std::vector cmds; + + static constexpr const std::size_t kMaxVertices = 65536; +}; + +class Draw2dQuadBuilder; +class Draw2dVerticesBuilder; + +/// @brief Buffered 2D drawing context +class Twodee +{ + std::vector lists_; + std::vector current_verts_; + std::vector current_indices_; + + friend class Draw2dQuadBuilder; + friend class Draw2dVerticesBuilder; + +public: + Twodee(); + Twodee(const Twodee&); + Twodee(Twodee&&) noexcept; + + Twodee& operator=(const Twodee&); + Twodee& operator=(Twodee&&) noexcept; + + Draw2dQuadBuilder begin_quad() noexcept; + Draw2dVerticesBuilder begin_verts() noexcept; + + typename std::vector::iterator begin() noexcept { return lists_.begin(); } + typename std::vector::iterator end() noexcept { return lists_.end(); } + typename std::vector::const_iterator begin() const noexcept { return lists_.cbegin(); } + typename std::vector::const_iterator end() const noexcept { return lists_.cend(); } + typename std::vector::const_iterator cbegin() const noexcept { return lists_.cbegin(); } + typename std::vector::const_iterator cend() const noexcept { return lists_.cend(); } +}; + +class Draw2dQuadBuilder +{ + Draw2dPatchQuad quad_; + Twodee& ctx_; + + Draw2dQuadBuilder(Twodee& ctx) : quad_ {}, ctx_ {ctx} {} + + friend class Twodee; + +public: + Draw2dQuadBuilder(const Draw2dQuadBuilder&) = delete; + Draw2dQuadBuilder(Draw2dQuadBuilder&&) = default; + Draw2dQuadBuilder& operator=(const Draw2dQuadBuilder&) = delete; + Draw2dQuadBuilder& operator=(Draw2dQuadBuilder&&) = default; + + Draw2dQuadBuilder& rect(float x, float y, float w, float h) + { + quad_.xmin = x; + quad_.xmax = x + w; + quad_.ymin = y; + quad_.ymax = y + h; + return *this; + } + + Draw2dQuadBuilder& flip(bool flip) + { + quad_.flip = flip; + return *this; + } + + Draw2dQuadBuilder& vflip(bool vflip) + { + quad_.vflip = vflip; + return *this; + } + + Draw2dQuadBuilder& clip(float xmin, float ymin, float xmax, float ymax) + { + quad_.clip_xmin = xmin; + quad_.clip_ymin = ymin; + quad_.clip_xmax = xmax; + quad_.clip_ymax = ymax; + quad_.clip = true; + return *this; + } + + Draw2dQuadBuilder& color(float r, float g, float b, float a) + { + quad_.r = r; + quad_.g = g; + quad_.b = b; + quad_.a = a; + return *this; + } + + Draw2dQuadBuilder& patch(const patch_t* patch) + { + quad_.patch = patch; + return *this; + } + + Draw2dQuadBuilder& blend(Draw2dBlend blend) + { + quad_.blend = blend; + return *this; + } + + Draw2dQuadBuilder& colormap(const uint8_t* colormap) + { + quad_.colormap = colormap; + return *this; + } + + void done(); +}; + +class Draw2dVerticesBuilder +{ + Draw2dVertices tris_; + Twodee& ctx_; + std::vector> verts_; + float r_ = 1.f; + float g_ = 1.f; + float b_ = 1.f; + float a_ = 1.f; + + Draw2dVerticesBuilder(Twodee& ctx) : tris_ {}, ctx_ {ctx} {} + + friend class Twodee; + +public: + Draw2dVerticesBuilder(const Draw2dVerticesBuilder&) = delete; + Draw2dVerticesBuilder(Draw2dVerticesBuilder&&) = default; + Draw2dVerticesBuilder& operator=(const Draw2dVerticesBuilder&) = delete; + Draw2dVerticesBuilder& operator=(Draw2dVerticesBuilder&&) = default; + + Draw2dVerticesBuilder& vert(float x, float y, float u = 0, float v = 0) + { + verts_.push_back({x, y, u, v}); + tris_.elements += 1; + return *this; + } + + Draw2dVerticesBuilder& color(float r, float g, float b, float a) + { + r_ = r; + g_ = g; + b_ = b; + a_ = a; + return *this; + } + + Draw2dVerticesBuilder& blend(Draw2dBlend blend) + { + tris_.blend = blend; + return *this; + } + + Draw2dVerticesBuilder& lines(bool lines) + { + tris_.lines = lines; + return *this; + } + + Draw2dVerticesBuilder& flat(lumpnum_t lump) + { + tris_.flat_lump = lump; + return *this; + } + + void done(); +}; + +inline Draw2dQuadBuilder Twodee::begin_quad() noexcept +{ + return Draw2dQuadBuilder(*this); +} + +inline Draw2dVerticesBuilder Twodee::begin_verts() noexcept +{ + return Draw2dVerticesBuilder(*this); +} + +} // namespace srb2::hwr2 + +#endif // __SRB2_HWR2_TWODEE_HPP__ diff --git a/src/i_video.h b/src/i_video.h index a80bd33dc..4da72efc0 100644 --- a/src/i_video.h +++ b/src/i_video.h @@ -130,10 +130,25 @@ extern boolean allow_fullscreen; */ void I_UpdateNoBlit(void); +/** \brief Begin a new Twodee frame. +*/ +void I_NewTwodeeFrame(void); + +/** \brief Begin a new dear imgui frame. +*/ +void I_NewImguiFrame(void); + /** \brief Update video system with updating frame */ void I_FinishUpdate(void); +void I_FinishUpdateWipeStartScreen(void); +void I_FinishUpdateWipeEndScreen(void); + +/** \brief Update video system during a wipe +*/ +void I_FinishUpdateWipe(void); + /** \brief I_FinishUpdate(), but vsync disabled */ void I_UpdateNoVsync(void); diff --git a/src/i_video_common.cpp b/src/i_video_common.cpp index 14f5e31ae..ef76d1e58 100644 --- a/src/i_video_common.cpp +++ b/src/i_video_common.cpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "i_video.h" #include @@ -7,30 +16,51 @@ #include #include "cxxutil.hpp" +#include "f_finale.h" +#include "hwr2/pass_blit_rect.hpp" #include "hwr2/pass_imgui.hpp" +#include "hwr2/pass_manager.hpp" +#include "hwr2/pass_postprocess.hpp" +#include "hwr2/pass_resource_managers.hpp" +#include "hwr2/pass_screenshot.hpp" #include "hwr2/pass_software.hpp" +#include "hwr2/pass_twodee.hpp" +#include "hwr2/twodee.hpp" #include "v_video.h" // KILL THIS WHEN WE KILL OLD OGL SUPPORT PLEASE +#include "d_netcmd.h" // kill +#include "discord.h" // kill +#include "doomstat.h" // kill +#include "s_sound.h" // kill #include "sdl/ogl_sdl.h" #include "st_stuff.h" // kill -#include "d_netcmd.h" // kill -#include "doomstat.h" // kill -#include "s_sound.h" // kill -#include "discord.h" // kill using namespace srb2; using namespace srb2::hwr2; using namespace srb2::rhi; -static SoftwareBlitPass g_sw_pass; -static ImguiPass g_imgui_pass; +namespace +{ +struct InternalPassData +{ + std::shared_ptr resource_passmanager; + std::shared_ptr normal_rendering; + std::shared_ptr wipe_capture_start_rendering; + std::shared_ptr wipe_capture_end_rendering; + std::shared_ptr wipe_rendering; +}; +} // namespace + +static std::unique_ptr g_passes; +static Rhi* g_last_known_rhi = nullptr; +static bool g_imgui_frame_active = false; Handle srb2::sys::g_current_rhi = kNullHandle; -static bool rhi_changed() +static bool rhi_changed(Rhi* rhi) { - return false; + return g_last_known_rhi != rhi; } #ifdef HWRENDER @@ -48,8 +78,7 @@ static void finish_legacy_ogl_update() if (cv_ticrate.value) SCR_DisplayTicRate(); - if (cv_showping.value && netgame && - ( consoleplayer != serverplayer || ! server_lagless )) + if (cv_showping.value && netgame && (consoleplayer != serverplayer || !server_lagless)) { if (server_lagless) { @@ -58,11 +87,8 @@ static void finish_legacy_ogl_update() } else { - for ( - player = 1; - player < MAXPLAYERS; - player++ - ){ + for (player = 1; player < MAXPLAYERS; player++) + { if (D_IsPlayerHumanAndGaming(player)) { SCR_DisplayLocalPing(); @@ -91,6 +117,242 @@ static void finish_legacy_ogl_update() } #endif +static InternalPassData build_pass_manager() +{ + auto framebuffer_manager = std::make_shared(); + auto palette_manager = std::make_shared(); + auto common_resources_manager = std::make_shared(); + auto flat_texture_manager = std::make_shared(); + auto resource_manager = std::make_shared(); + + resource_manager->insert("framebuffer_manager", framebuffer_manager); + resource_manager->insert("palette_manager", palette_manager); + resource_manager->insert("common_resources_manager", common_resources_manager); + resource_manager->insert("flat_texture_manager", flat_texture_manager); + + // Basic Rendering is responsible for drawing 3d, 2d, and postprocessing the image. + // This is drawn to an alternating internal color buffer. + // Normal Rendering will output the result via final composite and present. + // Wipe Start Screen and Wipe End Screen will save to special color buffers used for Wipe Rendering. + auto basic_rendering = std::make_shared(); + + auto software_pass = std::make_shared(); + auto blit_sw_pass = std::make_shared(palette_manager, true); + auto twodee = std::make_shared(); + twodee->flat_manager_ = flat_texture_manager; + twodee->data_ = make_twodee_pass_data(); + twodee->ctx_ = &g_2d; + auto pp_simple_blit_pass = std::make_shared(false); + auto screenshot_pass = std::make_shared(); + auto imgui_pass = std::make_shared(); + auto final_composite_pass = std::make_shared(true); + + basic_rendering->insert( + "3d_prepare", + [framebuffer_manager](PassManager& mgr, Rhi&) + { + const bool sw_enabled = rendermode == render_soft && gamestate != GS_NULL; + + mgr.set_pass_enabled("software", sw_enabled); + mgr.set_pass_enabled("blit_sw_prepare", sw_enabled); + mgr.set_pass_enabled("blit_sw", sw_enabled && !g_wipeskiprender); + } + ); + basic_rendering->insert("software", software_pass); + basic_rendering->insert( + "blit_sw_prepare", + [blit_sw_pass, software_pass, framebuffer_manager](PassManager&, Rhi&) + { + blit_sw_pass->set_texture(software_pass->screen_texture(), vid.width, vid.height); + blit_sw_pass->set_output(framebuffer_manager->main_color(), vid.width, vid.height, false, false); + } + ); + basic_rendering->insert("blit_sw", blit_sw_pass); + + basic_rendering->insert( + "2d_prepare", + [twodee, framebuffer_manager, palette_manager](PassManager& mgr, Rhi&) + { + twodee->output_ = framebuffer_manager->main_color(); + twodee->palette_manager_ = palette_manager; + twodee->output_width_ = vid.width; + twodee->output_height_ = vid.height; + } + ); + basic_rendering->insert("2d", twodee); + + basic_rendering->insert( + "pp_final_simple_blit_prepare", + [pp_simple_blit_pass, framebuffer_manager](PassManager&, Rhi&) + { + framebuffer_manager->swap_post(); + pp_simple_blit_pass->set_texture(framebuffer_manager->main_color(), vid.width, vid.height); + pp_simple_blit_pass + ->set_output(framebuffer_manager->current_post_color(), vid.width, vid.height, false, false); + } + ); + basic_rendering->insert("pp_final_simple_blit", pp_simple_blit_pass); + + basic_rendering->insert( + "screenshot_prepare", + [screenshot_pass, framebuffer_manager](PassManager&, Rhi&) + { + screenshot_pass->set_source(framebuffer_manager->current_post_color(), vid.width, vid.height); + } + ); + basic_rendering->insert("screenshot", screenshot_pass); + + // Composite-present takes the current postprocess result and outputs it to the default framebuffer. + // It also renders imgui and presents the screen. + auto composite_present_rendering = std::make_shared(); + + composite_present_rendering->insert( + "final_composite_prepare", + [final_composite_pass, framebuffer_manager](PassManager&, Rhi&) + { + final_composite_pass->set_texture(framebuffer_manager->current_post_color(), vid.width, vid.height); + final_composite_pass->set_output(kNullHandle, vid.realwidth, vid.realheight, true, false); + } + ); + composite_present_rendering->insert("final_composite", final_composite_pass); + composite_present_rendering->insert("imgui", imgui_pass); + composite_present_rendering->insert( + "present", + [](PassManager&, Rhi& rhi) {}, + [framebuffer_manager](PassManager&, Rhi& rhi) + { + g_imgui_frame_active = false; + rhi.present(); + rhi.finish(); + framebuffer_manager->reset_post(); + } + ); + + // Normal rendering combines basic rendering and composite-present. + auto normal_rendering = std::make_shared(); + + normal_rendering->insert("resource_manager", resource_manager); + normal_rendering->insert("basic_rendering", basic_rendering); + normal_rendering->insert("composite_present_rendering", composite_present_rendering); + + // Wipe Start Screen Capture rendering + auto wipe_capture_start_rendering = std::make_shared(); + auto wipe_start_blit = std::make_shared(); + + wipe_capture_start_rendering->insert("resource_manager", resource_manager); + wipe_capture_start_rendering->insert("basic_rendering", basic_rendering); + wipe_capture_start_rendering->insert( + "wipe_capture_prepare", + [framebuffer_manager, wipe_start_blit](PassManager&, Rhi&) + { + wipe_start_blit->set_texture(framebuffer_manager->previous_post_color(), vid.width, vid.height); + wipe_start_blit->set_output(framebuffer_manager->wipe_start_color(), vid.width, vid.height, false, true); + } + ); + wipe_capture_start_rendering->insert("wipe_capture", wipe_start_blit); + + // Wipe End Screen Capture rendering + auto wipe_capture_end_rendering = std::make_shared(); + auto wipe_end_blit = std::make_shared(); + auto wipe_end_blit_start_to_main = std::make_shared(); + + wipe_capture_end_rendering->insert("resource_manager", resource_manager); + wipe_capture_end_rendering->insert("basic_rendering", basic_rendering); + wipe_capture_end_rendering->insert( + "wipe_capture_prepare", + [framebuffer_manager, wipe_end_blit, wipe_end_blit_start_to_main](PassManager&, Rhi&) + { + wipe_end_blit->set_texture(framebuffer_manager->current_post_color(), vid.width, vid.height); + wipe_end_blit->set_output(framebuffer_manager->wipe_end_color(), vid.width, vid.height, false, true); + + wipe_end_blit_start_to_main->set_texture( + framebuffer_manager->wipe_start_color(), + vid.width, + vid.height + ); + wipe_end_blit_start_to_main->set_output( + framebuffer_manager->main_color(), + vid.width, + vid.height, + false, + true + ); + } + ); + wipe_capture_end_rendering->insert("wipe_capture", wipe_end_blit); + wipe_capture_end_rendering->insert("wipe_end_blit_start_to_main", wipe_end_blit_start_to_main); + + // Wipe rendering only runs the wipe shader on the start and end screens, and adds composite-present. + auto wipe_rendering = std::make_shared(); + + auto pp_wipe_pass = std::make_shared(); + + wipe_rendering->insert("resource_manager", resource_manager); + wipe_rendering->insert( + "pp_final_wipe_prepare", + [pp_wipe_pass, framebuffer_manager, common_resources_manager](PassManager&, Rhi&) + { + framebuffer_manager->swap_post(); + Handle start = framebuffer_manager->main_color(); + Handle end = framebuffer_manager->wipe_end_color(); + if (g_wipereverse) + { + std::swap(start, end); + } + pp_wipe_pass->set_start(start); + pp_wipe_pass->set_end(end); + pp_wipe_pass->set_target(framebuffer_manager->current_post_color(), vid.width, vid.height); + } + ); + wipe_rendering->insert("pp_final_wipe", pp_wipe_pass); + wipe_rendering->insert( + "screenshot_prepare", + [screenshot_pass, framebuffer_manager](PassManager&, Rhi&) + { + screenshot_pass->set_source(framebuffer_manager->current_post_color(), vid.width, vid.height); + } + ); + wipe_rendering->insert("screenshot", screenshot_pass); + wipe_rendering->insert("composite_present_rendering", composite_present_rendering); + + InternalPassData ret; + ret.resource_passmanager = resource_manager; + ret.normal_rendering = normal_rendering; + ret.wipe_capture_start_rendering = wipe_capture_start_rendering; + ret.wipe_capture_end_rendering = wipe_capture_end_rendering; + ret.wipe_rendering = wipe_rendering; + + return ret; +} + +void I_NewTwodeeFrame(void) +{ + g_2d = Twodee(); +} + +void I_NewImguiFrame(void) +{ + if (g_imgui_frame_active) + { + ImGui::EndFrame(); + g_imgui_frame_active = false; + } + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize.x = vid.realwidth; + io.DisplaySize.y = vid.realheight; + ImGui::NewFrame(); + g_imgui_frame_active = true; +} + +static void maybe_reinit_passes(Rhi* rhi) +{ + if (rhi_changed(rhi) || !g_passes) + { + g_last_known_rhi = rhi; + g_passes = std::make_unique(build_pass_manager()); + } +} + void I_FinishUpdate(void) { if (rendermode == render_none) @@ -106,19 +368,34 @@ void I_FinishUpdate(void) } #endif - // TODO move this to srb2loop - ImGuiIO& io = ImGui::GetIO(); - io.DisplaySize.x = vid.realwidth; - io.DisplaySize.y = vid.realheight; - ImGui::NewFrame(); + rhi::Rhi* rhi = sys::get_rhi(sys::g_current_rhi); - if (rhi_changed()) + if (rhi == nullptr) { - // reinitialize passes - g_sw_pass = SoftwareBlitPass(); - g_imgui_pass = ImguiPass(); + // ??? + return; } + maybe_reinit_passes(rhi); + + g_passes->normal_rendering->render(*rhi); +} + +void I_FinishUpdateWipeStartScreen(void) +{ + if (rendermode == render_none) + { + return; + } + +#ifdef HWRENDER + if (rendermode == render_opengl) + { + finish_legacy_ogl_update(); + return; + } +#endif + rhi::Rhi* rhi = sys::get_rhi(sys::g_current_rhi); if (rhi == nullptr) @@ -127,48 +404,63 @@ void I_FinishUpdate(void) return; } - // Prepare phase - if (rendermode == render_soft) - { - g_sw_pass.prepass(*rhi); - } - g_imgui_pass.prepass(*rhi); + maybe_reinit_passes(rhi); - // Transfer phase - Handle tc; - tc = rhi->begin_transfer(); - - if (rendermode == render_soft) - { - g_sw_pass.transfer(*rhi, tc); - } - g_imgui_pass.transfer(*rhi, tc); - - rhi->end_transfer(tc); - - // Graphics phase - Handle gc; - gc = rhi->begin_graphics(); - - // Standard drawing passes... - if (rendermode == render_soft) - { - g_sw_pass.graphics(*rhi, gc); - } - g_imgui_pass.graphics(*rhi, gc); - - rhi->end_graphics(gc); - - // Postpass phase - if (rendermode == render_soft) - { - g_sw_pass.postpass(*rhi); - } - g_imgui_pass.postpass(*rhi); - - // Present - - rhi->present(); - - rhi->finish(); + g_passes->wipe_capture_start_rendering->render(*rhi); +} + +void I_FinishUpdateWipeEndScreen(void) +{ + if (rendermode == render_none) + { + return; + } + +#ifdef HWRENDER + if (rendermode == render_opengl) + { + finish_legacy_ogl_update(); + return; + } +#endif + + rhi::Rhi* rhi = sys::get_rhi(sys::g_current_rhi); + + if (rhi == nullptr) + { + // ??? + return; + } + + maybe_reinit_passes(rhi); + + g_passes->wipe_capture_end_rendering->render(*rhi); +} + +void I_FinishUpdateWipe(void) +{ + if (rendermode == render_none) + { + return; + } + +#ifdef HWRENDER + if (rendermode == render_opengl) + { + finish_legacy_ogl_update(); + return; + } +#endif + + rhi::Rhi* rhi = sys::get_rhi(sys::g_current_rhi); + + if (rhi == nullptr) + { + // ??? + return; + } + + maybe_reinit_passes(rhi); + + g_passes->wipe_rendering->render(*rhi); } diff --git a/src/info.c b/src/info.c index 4807869e6..d0509e731 100644 --- a/src/info.c +++ b/src/info.c @@ -29311,6 +29311,60 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOCLIPTHING|MF_NOGRAVITY|MF_DONTENCOREMAP|MF_NOSQUISH, // flags S_NULL // raisestate }, + + { // MT_LOOPENDPOINT + 2020, // doomednum + S_INVISIBLE, // spawnstate + 1000, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 8, // reactiontime + sfx_None, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_NULL, // deathstate + S_NULL, // xdeathstate + sfx_None, // deathsound + 0, // speed + MAXRADIUS, // radius + 2*MAXRADIUS, // height + 0, // display offset + 100, // mass + 1, // damage + sfx_None, // activesound + MF_SPECIAL|MF_NOGRAVITY|MF_DONTENCOREMAP, // flags + S_NULL // raisestate + }, + + { // MT_LOOPCENTERPOINT + 2021, // doomednum + S_INVISIBLE, // spawnstate + 1000, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 8, // reactiontime + sfx_None, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_NULL, // deathstate + S_NULL, // xdeathstate + sfx_None, // deathsound + 0, // speed + 48*FRACUNIT, // radius + 32*FRACUNIT, // height + 0, // display offset + 100, // mass + 1, // damage + sfx_None, // activesound + MF_NOSECTOR|MF_NOBLOCKMAP|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_DONTENCOREMAP, // flags + S_NULL // raisestate + }, }; skincolor_t skincolors[MAXSKINCOLORS] = { diff --git a/src/info.h b/src/info.h index 1bac859b1..02e194cb6 100644 --- a/src/info.h +++ b/src/info.h @@ -6736,6 +6736,9 @@ typedef enum mobj_type MT_SPECIAL_UFO, MT_SPECIAL_UFO_PIECE, + MT_LOOPENDPOINT, + MT_LOOPCENTERPOINT, + MT_FIRSTFREESLOT, MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1, NUMMOBJTYPES diff --git a/src/k_collide.c b/src/k_collide.c index 33834e88f..ee773dfa3 100644 --- a/src/k_collide.c +++ b/src/k_collide.c @@ -474,10 +474,10 @@ boolean K_DropTargetCollide(mobj_t *t1, mobj_t *t2) { mobj_t *draggeddroptarget = (t1->type == MT_DROPTARGET_SHIELD) ? t1->target : NULL; - if ((t1->threshold > 0 && (t2->hitlag > 0 || !draggeddroptarget)) || (t2->threshold > 0 && t1->hitlag > 0)) + if ((t1->threshold > 0 && t2->hitlag > 0) || (t2->threshold > 0 && t1->hitlag > 0)) return true; - if (((t1->target == t2) || (t1->target == t2->target)) && (t1->threshold > 0 || (t2->type != MT_PLAYER && t2->threshold > 0))) + if (((t1->target == t2) || (t1->target == t2->target)) && ((t1->threshold > 0 && t2->type == MT_PLAYER) || (t2->type != MT_PLAYER && t2->threshold > 0))) return true; if (t1->health <= 0 || t2->health <= 0) @@ -711,7 +711,7 @@ boolean K_BubbleShieldCollide(mobj_t *t1, mobj_t *t2) } else { - if (!t2->threshold) + if (!t2->threshold || t2->type == MT_DROPTARGET) { if (!t2->momx && !t2->momy) { diff --git a/src/k_director.c b/src/k_director.c index 4f5737b4b..dd2f55ad6 100644 --- a/src/k_director.c +++ b/src/k_director.c @@ -25,6 +25,7 @@ void K_InitDirector(void) { INT32 playernum; + directorinfo.active = false; directorinfo.cooldown = SWITCHTIME; directorinfo.freeze = 0; directorinfo.attacker = 0; @@ -114,6 +115,11 @@ static boolean K_CanSwitchDirector(void) static void K_DirectorSwitch(INT32 player, boolean force) { + if (!directorinfo.active) + { + return; + } + if (P_IsDisplayPlayer(&players[player])) { return; @@ -218,11 +224,6 @@ void K_UpdateDirector(void) INT32 *displayplayerp = &displayplayers[0]; INT32 targetposition; - if (!cv_director.value) - { - return; - } - K_UpdateDirectorPositions(); if (directorinfo.cooldown > 0) { @@ -284,14 +285,26 @@ void K_UpdateDirector(void) target = directorinfo.sortedplayers[targetposition]; + // stop here since we're already viewing this player + if (*displayplayerp == target) + { + break; + } + + // if this is a splitscreen player, try next pair + if (P_IsDisplayPlayer(&players[target])) + { + continue; + } + // if we're certain the back half of the pair is actually in this position, try to switch - if (*displayplayerp != target && !players[target].positiondelay) + if (!players[target].positiondelay) { K_DirectorSwitch(target, false); } // even if we're not certain, if we're certain we're watching the WRONG player, try to switch - if (players[*displayplayerp].position != targetposition+1 && !players[target].positiondelay) + if (players[*displayplayerp].position != targetposition+1 && !players[*displayplayerp].positiondelay) { K_DirectorSwitch(target, false); } @@ -299,3 +312,13 @@ void K_UpdateDirector(void) break; } } + +void K_ToggleDirector(boolean active) +{ + if (directorinfo.active != active) + { + directorinfo.cooldown = 0; // switch immediately + } + + directorinfo.active = active; +} diff --git a/src/k_director.h b/src/k_director.h index 29bb01721..47b4b2265 100644 --- a/src/k_director.h +++ b/src/k_director.h @@ -12,6 +12,7 @@ extern "C" { extern struct directorinfo { + boolean active; // is view point switching enabled? tic_t cooldown; // how long has it been since we last switched? tic_t freeze; // when nonzero, fixed switch pending, freeze logic! INT32 attacker; // who to switch to when freeze delay elapses @@ -26,6 +27,7 @@ void K_InitDirector(void); void K_UpdateDirector(void); void K_DrawDirectorDebugger(void); void K_DirectorFollowAttack(player_t *player, mobj_t *inflictor, mobj_t *source); +void K_ToggleDirector(boolean active); #ifdef __cplusplus } // extern "C" diff --git a/src/k_hud.c b/src/k_hud.c index 2edb1f3d5..8fd7607ec 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -4639,6 +4639,77 @@ K_drawMiniPing (void) } } +static void K_DrawDirectorButton(INT32 idx, const char *label, patch_t *kp[2], INT32 textflags) +{ + INT32 flags = V_SNAPTORIGHT | V_SLIDEIN | V_SPLITSCREEN; + + const UINT8 anim_duration = 16; + const UINT8 anim = (leveltime % (anim_duration * 2)) < anim_duration; + + INT32 x = (BASEVIDWIDTH/2) - 10; + INT32 y = (idx * 16); + + if (r_splitscreen <= 1) + { + x = BASEVIDWIDTH - 60; + if (r_splitscreen == 0) + { + y += BASEVIDHEIGHT - 78; + } + } + + textflags |= (flags | V_6WIDTHSPACE | V_ALLOWLOWERCASE); + + V_DrawScaledPatch(x, y - 4, flags, kp[anim]); + V_DrawRightAlignedThinString(x - 2, y, textflags, label); +} + +static void K_drawDirectorHUD(void) +{ + const INT32 p = (splitscreen_partied[consoleplayer] ? splitscreen_party[consoleplayer] : g_localplayers)[R_GetViewNumber()]; + const char *itemtxt = "Join"; + UINT8 offs = 0; + + UINT8 numingame = 0; + UINT8 i; + + if (!LUA_HudEnabled(hud_textspectator)) + { + return; + } + + for (i = 0; i < MAXPLAYERS; i++) + if (playeringame[i] && !players[i].spectator) + numingame++; + + if (numingame > 1 && r_splitscreen == 0) // simplifies things a lot + { + K_DrawDirectorButton(1, "Next Player", kp_button_a[0], 0); + K_DrawDirectorButton(2, "Prev Player", kp_button_x[0], 0); + offs = 2; + } + + if (p == -1 || !playeringame[p] || players[p].spectator == false) + { + return; + } + + K_DrawDirectorButton(offs + 1, "Director", kp_button_r, + (directorinfo.active ? V_YELLOWMAP : 0)); + + if (players[p].flashing) + itemtxt = ". . ."; + else if (players[p].pflags & PF_WANTSTOJOIN) + itemtxt = "Cancel Join"; + + if (cv_maxplayers.value) + { + itemtxt = va("%s [%d/%d]", itemtxt, numingame, cv_maxplayers.value); + } + + K_DrawDirectorButton(0, itemtxt, kp_button_l, 0); +} + static void K_drawDistributionDebugger(void) { itemroulette_t rouletteData = {0}; @@ -4957,6 +5028,11 @@ void K_drawKartHUD(void) K_drawMiniPing(); } + if (displayplayers[viewnum] != g_localplayers[viewnum] && !demo.playback) + { + K_drawDirectorHUD(); + } + if (cv_kartdebugdistribution.value) K_drawDistributionDebugger(); diff --git a/src/k_kart.c b/src/k_kart.c index 5d4c7eb5d..4284c286d 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -5625,7 +5625,7 @@ void K_PuntMine(mobj_t *origMine, mobj_t *punter) spd = FixedMul(82 * punter->scale, K_GetKartGameSpeedScalar(gamespeed)); // Avg Speed is 41 in Normal - mine->flags |= MF_NOCLIPTHING; + mine->flags |= (MF_NOCLIP|MF_NOCLIPTHING); P_SetMobjState(mine, S_SSMINE_AIR1); mine->threshold = 10; @@ -5637,7 +5637,7 @@ void K_PuntMine(mobj_t *origMine, mobj_t *punter) //K_SetHitLagForObjects(punter, mine, 5); - mine->flags &= ~MF_NOCLIPTHING; + mine->flags &= ~(MF_NOCLIP|MF_NOCLIPTHING); } #define THUNDERRADIUS 320 @@ -5930,9 +5930,7 @@ void K_DoInvincibility(player_t *player, tic_t time) P_SetScale(overlay, player->mo->scale); } - player->invincibilitytimer += time; - - if (P_IsLocalPlayer(player) == true) + if (P_IsLocalPlayer(player) == true && player->invincibilitytimer == 0) { S_ChangeMusicSpecial("kinvnc"); } @@ -5941,6 +5939,8 @@ void K_DoInvincibility(player_t *player, tic_t time) S_StartSound(player->mo, sfx_alarmi); } + player->invincibilitytimer += time; + P_RestoreMusic(player); } @@ -6150,11 +6150,11 @@ void K_DropHnextList(player_t *player, boolean keepshields) { fixed_t radius = FixedMul(work->info->radius, dropwork->destscale); radius = FixedHypot(player->mo->radius, player->mo->radius) + FixedHypot(radius, radius); // mobj's distance from its Target, or Radius. - dropwork->flags |= MF_NOCLIPTHING; + dropwork->flags |= (MF_NOCLIP|MF_NOCLIPTHING); work->momx = FixedMul(FINECOSINE(work->angle>>ANGLETOFINESHIFT), radius); work->momy = FixedMul(FINESINE(work->angle>>ANGLETOFINESHIFT), radius); P_MoveOrigin(dropwork, player->mo->x + work->momx, player->mo->y + work->momy, player->mo->z); - dropwork->flags &= ~MF_NOCLIPTHING; + dropwork->flags &= ~(MF_NOCLIP|MF_NOCLIPTHING); } dropwork->flags2 |= MF2_AMBUSH; @@ -10099,7 +10099,7 @@ void K_UnsetItemOut(player_t *player) void K_MoveKartPlayer(player_t *player, boolean onground) { ticcmd_t *cmd = &player->cmd; - boolean ATTACK_IS_DOWN = ((cmd->buttons & BT_ATTACK) && !(player->oldcmd.buttons & BT_ATTACK)); + boolean ATTACK_IS_DOWN = ((cmd->buttons & BT_ATTACK) && !(player->oldcmd.buttons & BT_ATTACK) && (player->respawn.state == RESPAWNST_NONE)); boolean HOLDING_ITEM = (player->pflags & (PF_ITEMOUT|PF_EGGMANOUT)); boolean NO_HYUDORO = (player->stealingtimer == 0); @@ -10540,11 +10540,11 @@ void K_MoveKartPlayer(player_t *player, boolean onground) S_StartSound(player->mo, sfx_alarmg); } - P_RestoreMusic(player); - player->growshrinktimer = max(0, player->growshrinktimer); player->growshrinktimer += ((gametyperules & GTR_CLOSERPLAYERS) ? 8 : 12) * TICRATE; + P_RestoreMusic(player); + S_StartSound(player->mo, sfx_kc5a); player->itemamount--; @@ -11036,18 +11036,6 @@ void K_MoveKartPlayer(player_t *player, boolean onground) { player->pflags &= ~PF_AIRFAILSAFE; } - - // Play the starting countdown sounds - if (player == &players[g_localplayers[0]]) // Don't play louder in splitscreen - { - if ((leveltime == starttime-(3*TICRATE)) || (leveltime == starttime-(2*TICRATE)) || (leveltime == starttime-TICRATE)) - S_StartSound(NULL, sfx_s3ka7); - - if (leveltime == starttime-(3*TICRATE)) - S_FadeOutStopMusic(3500); - else if (leveltime == starttime) - S_StartSound(NULL, sfx_s3kad); - } } void K_CheckSpectateStatus(void) diff --git a/src/k_menudraw.c b/src/k_menudraw.c index ff5becb1b..49fd7a8d7 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -517,10 +517,7 @@ void M_Drawer(void) } else if (!WipeInAction && currentMenu != &PAUSE_PlaybackMenuDef) { - if (rendermode == render_opengl) // OGL can't handle what SW is doing so let's fake it; - V_DrawFadeScreen(122, 3); // palette index aproximation... - else // Software can keep its unique fade - V_DrawCustomFadeScreen("FADEMAP0", 4); // now that's more readable with a faded background (yeah like Quake...) + V_DrawFadeScreen(122, 3); } if (currentMenu->drawroutine) @@ -556,7 +553,7 @@ void M_Drawer(void) if (menuwipe) { F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_menu_final], false, "FADEMAP0", true, false); + F_RunWipe(wipe_menu_final, wipedefs[wipe_menu_final], false, "FADEMAP0", true, false); menuwipe = false; } @@ -4908,7 +4905,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) unlockable_t *ref = NULL; UINT8 *colormap = NULL; UINT16 specialmap = NEXTMAP_INVALID; - + if (challengesmenu.currentunlock >= MAXUNLOCKABLES) { return; diff --git a/src/k_menufunc.c b/src/k_menufunc.c index 8ac717d9c..4300ef933 100644 --- a/src/k_menufunc.c +++ b/src/k_menufunc.c @@ -602,7 +602,7 @@ void M_SetupNextMenu(menu_t *menudef, boolean notransition) F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_menu_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_menu_toblack, wipedefs[wipe_menu_toblack], false, "FADEMAP0", false, false); } } @@ -1035,7 +1035,7 @@ void M_Ticker(void) F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_menu_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_menu_toblack, wipedefs[wipe_menu_toblack], false, "FADEMAP0", false, false); } M_SetupNextMenu(menutransition.endmenu, true); diff --git a/src/k_objects.h b/src/k_objects.h index 05debb889..922764c94 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -2,6 +2,8 @@ #ifndef k_objects_H #define k_objects_H +#include "taglist.h" + #ifdef __cplusplus extern "C" { #endif @@ -92,6 +94,13 @@ boolean Obj_ItemSpotIsAvailable(const mobj_t *spot); void Obj_ItemSpotAssignMonitor(mobj_t *spot, mobj_t *monitor); void Obj_ItemSpotUpdate(mobj_t *spot); +/* Loops */ +mobj_t *Obj_FindLoopCenter(const mtag_t tag); +void Obj_InitLoopEndpoint(mobj_t *end, mobj_t *anchor); +void Obj_InitLoopCenter(mobj_t *center); +void Obj_LinkLoopAnchor(mobj_t *anchor, mobj_t *center, UINT8 type); +void Obj_LoopEndpointCollide(mobj_t *special, mobj_t *toucher); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/k_respawn.c b/src/k_respawn.c index 0997c2b40..cb08408a2 100644 --- a/src/k_respawn.c +++ b/src/k_respawn.c @@ -344,7 +344,10 @@ static void K_MovePlayerToRespawnPoint(player_t *player) player->mo->momx = player->mo->momy = player->mo->momz = 0; - player->flashing = 2; + // 3 because this timer counts down afterward, in + // P_PlayerThink. flashing must be > 1 after it has + // counted down in order to flicker the player sprite. + player->flashing = 3; //player->nocontrol = max(2, player->nocontrol); if (leveltime % 8 == 0 && !mapreset) @@ -813,6 +816,7 @@ void K_RespawnChecker(player_t *player) return; case RESPAWNST_DROP: player->mo->momx = player->mo->momy = 0; + player->flashing = 3; if (player->respawn.timer > 0) { player->mo->momz = 0; diff --git a/src/lua_baselib.c b/src/lua_baselib.c index 4dead2615..654dc71d0 100644 --- a/src/lua_baselib.c +++ b/src/lua_baselib.c @@ -35,6 +35,7 @@ #include "k_menu.h" // Player Setup menu color stuff #include "p_spec.h" // P_StartQuake #include "i_system.h" // I_GetPreciseTime, I_GetPrecisePrecision +#include "hu_stuff.h" // for the cecho #include "lua_script.h" #include "lua_libs.h" @@ -3879,6 +3880,14 @@ static int lib_getTimeMicros(lua_State *L) return 1; } +static int lib_startTitlecardCecho(lua_State *L) +{ + const char *str = luaL_checkstring(L, 1); + HU_DoTitlecardCEcho(str); + + return 1; +} + static luaL_Reg lib[] = { {"print", lib_print}, {"chatprint", lib_chatprint}, @@ -4160,7 +4169,10 @@ static luaL_Reg lib[] = { {"K_InitBossHealthBar", lib_kInitBossHealthBar}, {"K_UpdateBossHealthBar", lib_kUpdateBossHealthBar}, {"K_DeclareWeakspot", lib_kDeclareWeakspot}, - + + // hu_stuff technically? + {"HU_DoTitlecardCEcho", lib_startTitlecardCecho}, + {NULL, NULL} }; diff --git a/src/m_anigif.c b/src/m_anigif.c index a6c830fa3..0bd14f422 100644 --- a/src/m_anigif.c +++ b/src/m_anigif.c @@ -508,7 +508,7 @@ static size_t gifframe_size = 8192; #ifdef HWRENDER static colorlookup_t gif_colorlookup; -static void GIF_rgbconvert(UINT8 *linear, UINT8 *scr) +static void GIF_rgbconvert(const UINT8 *linear, UINT8 *scr) { UINT8 r, g, b; size_t src = 0, dest = 0; @@ -532,13 +532,18 @@ static void GIF_rgbconvert(UINT8 *linear, UINT8 *scr) // GIF_framewrite // writes a frame into the file. // -static void GIF_framewrite(void) +static void GIF_framewrite(INT32 input_width, INT32 input_height, const UINT8 *input) { UINT8 *p; UINT8 *movie_screen = screens[2]; INT32 blitx, blity, blitw, blith; boolean palchanged; + (void)input_width; + (void)input_height; + + I_Assert(input_width == vid.width && input_height == vid.height); + if (!gifframe_data) gifframe_data = Z_Malloc(gifframe_size, PU_STATIC, NULL); p = gifframe_data; @@ -565,7 +570,10 @@ static void GIF_framewrite(void) // blit to temp screen if (rendermode == render_soft) - I_ReadScreen(movie_screen); + { + I_Assert(input != NULL); + GIF_rgbconvert(input, movie_screen); + } #ifdef HWRENDER else if (rendermode == render_opengl) { @@ -594,7 +602,10 @@ static void GIF_framewrite(void) // Copy the first frame into the movie screen // OpenGL already does the same above. if (gif_frames == 0 && rendermode == render_soft) - I_ReadScreen(movie_screen); + { + I_Assert(input != NULL); + GIF_rgbconvert(input, screens[0]); + } movie_screen = screens[0]; } @@ -751,7 +762,16 @@ INT32 GIF_open(const char *filename) void GIF_frame(void) { // there's not much actually needed here, is there. - GIF_framewrite(); + GIF_framewrite(vid.width, vid.height, NULL); +} + +// +// GIF_frame_rgb24 +// writes a frame into the output gif, with existing image data +// +void GIF_frame_rgb24(INT32 width, INT32 height, const UINT8 *buffer) +{ + GIF_framewrite(width, height, buffer); } // diff --git a/src/m_anigif.h b/src/m_anigif.h index 9ee209aea..34f149a88 100644 --- a/src/m_anigif.h +++ b/src/m_anigif.h @@ -28,6 +28,7 @@ extern "C" { #ifdef HAVE_ANIGIF INT32 GIF_open(const char *filename); void GIF_frame(void); +void GIF_frame_rgb24(INT32 width, INT32 height, const UINT8 *buffer); INT32 GIF_close(void); #endif diff --git a/src/m_avrecorder.cpp b/src/m_avrecorder.cpp index 7491fb195..7467e388d 100644 --- a/src/m_avrecorder.cpp +++ b/src/m_avrecorder.cpp @@ -229,24 +229,3 @@ void M_AVRecorder_DrawFrameRate(void) 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 pal(&pLocalPalette[std::max(st_palette, 0) * 256], 256); - tcb::span 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)); -} diff --git a/src/m_avrecorder.h b/src/m_avrecorder.h index 9cce8349c..21d635c5f 100644 --- a/src/m_avrecorder.h +++ b/src/m_avrecorder.h @@ -34,9 +34,6 @@ 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, diff --git a/src/m_memcpy.c b/src/m_memcpy.c new file mode 100644 index 000000000..d24f00f94 --- /dev/null +++ b/src/m_memcpy.c @@ -0,0 +1,440 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 1993-1996 by id Software, Inc. +// Copyright (C) 1998-2000 by DooM Legacy Team. +// Copyright (C) 1999-2023 by Sonic Team Junior. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file m_memcpy.c +/// \brief X86 optimized implementations of M_Memcpy + +#include "doomdef.h" +#include "m_misc.h" + +#if defined (__GNUC__) && defined (__i386__) // from libkwave, under GPL +// Alam: note libkwave memcpy code comes from mplayer's libvo/aclib_template.c, r699 + +/* for small memory blocks (<256 bytes) this version is faster */ +#define small_memcpy(dest,src,n)\ +{\ +register unsigned long int dummy;\ +__asm__ __volatile__(\ + "cld\n\t"\ + "rep; movsb"\ + :"=&D"(dest), "=&S"(src), "=&c"(dummy)\ + :"0" (dest), "1" (src),"2" (n)\ + : "memory", "cc");\ +} + +/* linux kernel __memcpy (from: /include/asm/string.h) */ +ATTRINLINE static FUNCINLINE void *__memcpy (void *dest, const void * src, size_t n) +{ + int d0, d1, d2; + + if ( n < 4 ) + { + small_memcpy(dest, src, n); + } + else + { + __asm__ __volatile__ ( + "rep ; movsl;" + "testb $2,%b4;" + "je 1f;" + "movsw;" + "1:\ttestb $1,%b4;" + "je 2f;" + "movsb;" + "2:" + : "=&c" (d0), "=&D" (d1), "=&S" (d2) + :"0" (n/4), "q" (n),"1" ((long) dest),"2" ((long) src) + : "memory"); + } + + return dest; +} + +#define SSE_MMREG_SIZE 16 +#define MMX_MMREG_SIZE 8 + +#define MMX1_MIN_LEN 0x800 /* 2K blocks */ +#define MIN_LEN 0x40 /* 64-byte blocks */ + +/* SSE note: i tried to move 128 bytes a time instead of 64 but it +didn't make any measureable difference. i'm using 64 for the sake of +simplicity. [MF] */ +static /*FUNCTARGET("sse2")*/ void *sse_cpy(void * dest, const void * src, size_t n) +{ + void *retval = dest; + size_t i; + + /* PREFETCH has effect even for MOVSB instruction ;) */ + __asm__ __volatile__ ( + "prefetchnta (%0);" + "prefetchnta 32(%0);" + "prefetchnta 64(%0);" + "prefetchnta 96(%0);" + "prefetchnta 128(%0);" + "prefetchnta 160(%0);" + "prefetchnta 192(%0);" + "prefetchnta 224(%0);" + "prefetchnta 256(%0);" + "prefetchnta 288(%0);" + : : "r" (src) ); + + if (n >= MIN_LEN) + { + register unsigned long int delta; + /* Align destinition to MMREG_SIZE -boundary */ + delta = ((unsigned long int)dest)&(SSE_MMREG_SIZE-1); + if (delta) + { + delta=SSE_MMREG_SIZE-delta; + n -= delta; + small_memcpy(dest, src, delta); + } + i = n >> 6; /* n/64 */ + n&=63; + if (((unsigned long)src) & 15) + /* if SRC is misaligned */ + for (; i>0; i--) + { + __asm__ __volatile__ ( + "prefetchnta 320(%0);" + "prefetchnta 352(%0);" + "movups (%0), %%xmm0;" + "movups 16(%0), %%xmm1;" + "movups 32(%0), %%xmm2;" + "movups 48(%0), %%xmm3;" + "movntps %%xmm0, (%1);" + "movntps %%xmm1, 16(%1);" + "movntps %%xmm2, 32(%1);" + "movntps %%xmm3, 48(%1);" + :: "r" (src), "r" (dest) : "memory"); + src = (const unsigned char *)src + 64; + dest = (unsigned char *)dest + 64; + } + else + /* + Only if SRC is aligned on 16-byte boundary. + It allows to use movaps instead of movups, which required data + to be aligned or a general-protection exception (#GP) is generated. + */ + for (; i>0; i--) + { + __asm__ __volatile__ ( + "prefetchnta 320(%0);" + "prefetchnta 352(%0);" + "movaps (%0), %%xmm0;" + "movaps 16(%0), %%xmm1;" + "movaps 32(%0), %%xmm2;" + "movaps 48(%0), %%xmm3;" + "movntps %%xmm0, (%1);" + "movntps %%xmm1, 16(%1);" + "movntps %%xmm2, 32(%1);" + "movntps %%xmm3, 48(%1);" + :: "r" (src), "r" (dest) : "memory"); + src = ((const unsigned char *)src) + 64; + dest = ((unsigned char *)dest) + 64; + } + /* since movntq is weakly-ordered, a "sfence" + * is needed to become ordered again. */ + __asm__ __volatile__ ("sfence":::"memory"); + /* enables to use FPU */ + __asm__ __volatile__ ("emms":::"memory"); + } + /* + * Now do the tail of the block + */ + if (n) __memcpy(dest, src, n); + return retval; +} + +static FUNCTARGET("mmx") void *mmx2_cpy(void *dest, const void *src, size_t n) +{ + void *retval = dest; + size_t i; + + /* PREFETCH has effect even for MOVSB instruction ;) */ + __asm__ __volatile__ ( + "prefetchnta (%0);" + "prefetchnta 32(%0);" + "prefetchnta 64(%0);" + "prefetchnta 96(%0);" + "prefetchnta 128(%0);" + "prefetchnta 160(%0);" + "prefetchnta 192(%0);" + "prefetchnta 224(%0);" + "prefetchnta 256(%0);" + "prefetchnta 288(%0);" + : : "r" (src)); + + if (n >= MIN_LEN) + { + register unsigned long int delta; + /* Align destinition to MMREG_SIZE -boundary */ + delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1); + if (delta) + { + delta=MMX_MMREG_SIZE-delta; + n -= delta; + small_memcpy(dest, src, delta); + } + i = n >> 6; /* n/64 */ + n&=63; + for (; i>0; i--) + { + __asm__ __volatile__ ( + "prefetchnta 320(%0);" + "prefetchnta 352(%0);" + "movq (%0), %%mm0;" + "movq 8(%0), %%mm1;" + "movq 16(%0), %%mm2;" + "movq 24(%0), %%mm3;" + "movq 32(%0), %%mm4;" + "movq 40(%0), %%mm5;" + "movq 48(%0), %%mm6;" + "movq 56(%0), %%mm7;" + "movntq %%mm0, (%1);" + "movntq %%mm1, 8(%1);" + "movntq %%mm2, 16(%1);" + "movntq %%mm3, 24(%1);" + "movntq %%mm4, 32(%1);" + "movntq %%mm5, 40(%1);" + "movntq %%mm6, 48(%1);" + "movntq %%mm7, 56(%1);" + :: "r" (src), "r" (dest) : "memory"); + src = ((const unsigned char *)src) + 64; + dest = ((unsigned char *)dest) + 64; + } + /* since movntq is weakly-ordered, a "sfence" + * is needed to become ordered again. */ + __asm__ __volatile__ ("sfence":::"memory"); + __asm__ __volatile__ ("emms":::"memory"); + } + /* + * Now do the tail of the block + */ + if (n) __memcpy(dest, src, n); + return retval; +} + +static FUNCTARGET("mmx") void *mmx1_cpy(void *dest, const void *src, size_t n) //3DNOW +{ + void *retval = dest; + size_t i; + + /* PREFETCH has effect even for MOVSB instruction ;) */ + __asm__ __volatile__ ( + "prefetch (%0);" + "prefetch 32(%0);" + "prefetch 64(%0);" + "prefetch 96(%0);" + "prefetch 128(%0);" + "prefetch 160(%0);" + "prefetch 192(%0);" + "prefetch 224(%0);" + "prefetch 256(%0);" + "prefetch 288(%0);" + : : "r" (src)); + + if (n >= MMX1_MIN_LEN) + { + register unsigned long int delta; + /* Align destinition to MMREG_SIZE -boundary */ + delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1); + if (delta) + { + delta=MMX_MMREG_SIZE-delta; + n -= delta; + small_memcpy(dest, src, delta); + } + i = n >> 6; /* n/64 */ + n&=63; + for (; i>0; i--) + { + __asm__ __volatile__ ( + "prefetch 320(%0);" + "prefetch 352(%0);" + "movq (%0), %%mm0;" + "movq 8(%0), %%mm1;" + "movq 16(%0), %%mm2;" + "movq 24(%0), %%mm3;" + "movq 32(%0), %%mm4;" + "movq 40(%0), %%mm5;" + "movq 48(%0), %%mm6;" + "movq 56(%0), %%mm7;" + "movq %%mm0, (%1);" + "movq %%mm1, 8(%1);" + "movq %%mm2, 16(%1);" + "movq %%mm3, 24(%1);" + "movq %%mm4, 32(%1);" + "movq %%mm5, 40(%1);" + "movq %%mm6, 48(%1);" + "movq %%mm7, 56(%1);" + :: "r" (src), "r" (dest) : "memory"); + src = ((const unsigned char *)src) + 64; + dest = ((unsigned char *)dest) + 64; + } + __asm__ __volatile__ ("femms":::"memory"); // same as mmx_cpy() but with a femms + } + /* + * Now do the tail of the block + */ + if (n) __memcpy(dest, src, n); + return retval; +} +#endif + +// Alam: why? memcpy may be __cdecl/_System and our code may be not the same type +static void *cpu_cpy(void *dest, const void *src, size_t n) +{ + if (src == NULL) + { + CONS_Debug(DBG_MEMORY, "Memcpy from 0x0?!: %p %p %s\n", dest, src, sizeu1(n)); + return dest; + } + + if(dest == NULL) + { + CONS_Debug(DBG_MEMORY, "Memcpy to 0x0?!: %p %p %s\n", dest, src, sizeu1(n)); + return dest; + } + + return memcpy(dest, src, n); +} + +static /*FUNCTARGET("mmx")*/ void *mmx_cpy(void *dest, const void *src, size_t n) +{ +#if defined (_MSC_VER) && defined (_X86_) + _asm + { + mov ecx, [n] + mov esi, [src] + mov edi, [dest] + shr ecx, 6 // mit mmx: 64bytes per iteration + jz lower_64 // if lower than 64 bytes + loop_64: // MMX transfers multiples of 64bytes + movq mm0, 0[ESI] // read sources + movq mm1, 8[ESI] + movq mm2, 16[ESI] + movq mm3, 24[ESI] + movq mm4, 32[ESI] + movq mm5, 40[ESI] + movq mm6, 48[ESI] + movq mm7, 56[ESI] + + movq 0[EDI], mm0 // write destination + movq 8[EDI], mm1 + movq 16[EDI], mm2 + movq 24[EDI], mm3 + movq 32[EDI], mm4 + movq 40[EDI], mm5 + movq 48[EDI], mm6 + movq 56[EDI], mm7 + + add esi, 64 + add edi, 64 + dec ecx + jnz loop_64 + emms // close mmx operation + lower_64:// transfer rest of buffer + mov ebx,esi + sub ebx,src + mov ecx,[n] + sub ecx,ebx + shr ecx, 3 // multiples of 8 bytes + jz lower_8 + loop_8: + movq mm0, [esi] // read source + movq [edi], mm0 // write destination + add esi, 8 + add edi, 8 + dec ecx + jnz loop_8 + emms // close mmx operation + lower_8: + mov ebx,esi + sub ebx,src + mov ecx,[n] + sub ecx,ebx + rep movsb + mov eax, [dest] // return dest + } +#elif defined (__GNUC__) && defined (__i386__) + void *retval = dest; + size_t i; + + if (n >= MMX1_MIN_LEN) + { + register unsigned long int delta; + /* Align destinition to MMREG_SIZE -boundary */ + delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1); + if (delta) + { + delta=MMX_MMREG_SIZE-delta; + n -= delta; + small_memcpy(dest, src, delta); + } + i = n >> 6; /* n/64 */ + n&=63; + for (; i>0; i--) + { + __asm__ __volatile__ ( + "movq (%0), %%mm0;" + "movq 8(%0), %%mm1;" + "movq 16(%0), %%mm2;" + "movq 24(%0), %%mm3;" + "movq 32(%0), %%mm4;" + "movq 40(%0), %%mm5;" + "movq 48(%0), %%mm6;" + "movq 56(%0), %%mm7;" + "movq %%mm0, (%1);" + "movq %%mm1, 8(%1);" + "movq %%mm2, 16(%1);" + "movq %%mm3, 24(%1);" + "movq %%mm4, 32(%1);" + "movq %%mm5, 40(%1);" + "movq %%mm6, 48(%1);" + "movq %%mm7, 56(%1);" + :: "r" (src), "r" (dest) : "memory"); + src = ((const unsigned char *)src) + 64; + dest = ((unsigned char *)dest) + 64; + } + __asm__ __volatile__ ("emms":::"memory"); + } + /* + * Now do the tail of the block + */ + if (n) __memcpy(dest, src, n); + return retval; +#else + return cpu_cpy(dest, src, n); +#endif +} + +void *(*M_Memcpy)(void* dest, const void* src, size_t n) = cpu_cpy; + +/** Memcpy that uses MMX, 3DNow, MMXExt or even SSE + * Do not use on overlapped memory, use memmove for that + */ +void M_SetupMemcpy(void) +{ +#if defined (__GNUC__) && defined (__i386__) + if (R_SSE2) + M_Memcpy = sse_cpy; + else if (R_MMXExt) + M_Memcpy = mmx2_cpy; + else if (R_3DNow) + M_Memcpy = mmx1_cpy; + else +#endif + if (R_MMX) + M_Memcpy = mmx_cpy; +#if 0 + M_Memcpy = cpu_cpy; +#endif +} diff --git a/src/m_misc.c b/src/m_misc.cpp similarity index 85% rename from src/m_misc.c rename to src/m_misc.cpp index 53e3534ac..0873a9777 100644 --- a/src/m_misc.c +++ b/src/m_misc.cpp @@ -8,7 +8,7 @@ // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- -/// \file m_misc.h +/// \file m_misc.cpp /// \brief Commonly used routines /// Default config file, PCX screenshots, file i/o @@ -23,6 +23,7 @@ #include #endif +#include #include // Extended map support. @@ -44,7 +45,10 @@ #include "command.h" // cv_execversion #include "m_anigif.h" +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #include "m_avrecorder.h" +#include "m_avrecorder.hpp" +#endif // So that the screenshot menu auto-updates... #include "k_menu.h" @@ -308,7 +312,7 @@ size_t FIL_ReadFileTag(char const *name, UINT8 **buffer, INT32 tag) length = ftell(handle); fseek(handle,0,SEEK_SET); - buf = Z_Malloc(length + 1, tag, NULL); + buf = static_cast(Z_Malloc(length + 1, tag, NULL)); count = fread(buf, 1, length, handle); fclose(handle); @@ -472,7 +476,7 @@ void M_SaveJoinedIPs(void) { FILE *f = NULL; UINT8 i; - const char *filepath = va("%s"PATHSEP"%s", srb2home, IPLOGFILE); + const char *filepath = va("%s" PATHSEP "%s", srb2home, IPLOGFILE); if (!*joinedIPlist[0][0]) return; // Don't bother, there's nothing to save. @@ -506,7 +510,7 @@ void M_LoadJoinedIPs(void) char *s; char buffer[2*(MAX_LOGIP+1)]; - filepath = va("%s"PATHSEP"%s", srb2home, IPLOGFILE); + filepath = va("%s" PATHSEP "%s", srb2home, IPLOGFILE); f = fopen(filepath, "r"); if (f == NULL) @@ -774,8 +778,8 @@ static void M_CreateScreenShotPalette(void) for (i = 0, j = 0; i < 768; i += 3, j++) { RGBA_t locpal = ((cv_screenshot_colorprofile.value) - ? pLocalPalette[(max(st_palette,0)*256)+j] - : pMasterPalette[(max(st_palette,0)*256)+j]); + ? pLocalPalette[(std::max(st_palette,0)*256)+j] + : pMasterPalette[(std::max(st_palette,0)*256)+j]); screenshot_palette[i] = locpal.s.red; screenshot_palette[i+1] = locpal.s.green; screenshot_palette[i+2] = locpal.s.blue; @@ -854,7 +858,7 @@ static void M_PNGhdr(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png_ const png_byte png_interlace = PNG_INTERLACE_NONE; //PNG_INTERLACE_ADAM7 if (palette) { - png_colorp png_PLTE = png_malloc(png_ptr, sizeof(png_color)*256); //palette + png_colorp png_PLTE = static_cast(png_malloc(png_ptr, sizeof(png_color)*256)); //palette png_uint_16 i; const png_byte *pal = palette; @@ -975,8 +979,8 @@ static void M_PNGText(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png static inline void M_PNGImage(png_structp png_ptr, png_infop png_info_ptr, PNG_CONST png_uint_32 height, png_bytep png_buf) { - png_uint_32 pitch = png_get_rowbytes(png_ptr, png_info_ptr); - png_bytepp row_pointers = png_malloc(png_ptr, height* sizeof (png_bytep)); + png_uint_32 pitch = png_get_rowbytes(png_ptr, static_cast(png_info_ptr)); + png_bytepp row_pointers = static_cast(png_malloc(png_ptr, height* sizeof (png_bytep))); png_uint_32 y; for (y = 0; y < height; y++) { @@ -1298,6 +1302,9 @@ static inline moviemode_t M_StartMovieGIF(const char *pathname) static inline moviemode_t M_StartMovieAVRecorder(const char *pathname) { +#ifndef SRB2_CONFIG_ENABLE_WEBM_MOVIES + return MM_OFF; +#else const char *ext = M_AVRecorder_GetFileExtension(); const char *freename; @@ -1313,6 +1320,7 @@ static inline moviemode_t M_StartMovieAVRecorder(const char *pathname) } return MM_AVRECORDER; +#endif } void M_StartMovie(void) @@ -1334,7 +1342,7 @@ void M_StartMovie(void) if (cv_movie_option.value != 3) { - strcat(pathname, PATHSEP"media"PATHSEP"movies"PATHSEP); + strcat(pathname, PATHSEP "media" PATHSEP "movies" PATHSEP); M_MkdirEach(pathname, M_PathParts(pathname) - 2, 0755); } @@ -1365,36 +1373,45 @@ 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"); +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES else if (moviemode == MM_AVRECORDER) { CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), M_AVRecorder_GetCurrentFormat()); M_AVRecorder_PrintCurrentConfiguration(); } +#endif //singletics = (moviemode != MM_OFF); #endif } -void M_SaveFrame(void) +static void M_SaveFrame_AVRecorder(uint32_t width, uint32_t height, tcb::span data); + +void M_LegacySaveFrame(void) { #if NUMSCREENS > 2 + // TODO: until HWR2 replaces legacy OpenGL renderer, this + // function still needs to called for OpenGL. +#ifdef HWRENDER + if (rendermode != render_opengl) +#endif + { + return; + } + // paranoia: should be unnecessary without singletics static tic_t oldtic = 0; +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES 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; } - return; } +#endif // skip interpolated frames for other modes if (oldtic == I_GetTime()) @@ -1444,6 +1461,15 @@ void M_SaveFrame(void) } #else moviemode = MM_OFF; +#endif + return; + case MM_AVRECORDER: +#if defined(SRB2_CONFIG_ENABLE_WEBM_MOVIES) && defined(HWRENDER) + { + UINT8 *linear = HWR_GetScreenshot(); + M_SaveFrame_AVRecorder(vid.width, vid.height, tcb::as_bytes(tcb::span(linear, 3 * vid.width * vid.height))); + free(linear); + } #endif return; default: @@ -1452,6 +1478,64 @@ void M_SaveFrame(void) #endif } +static void M_SaveFrame_GIF(uint32_t width, uint32_t height, tcb::span data) +{ + if (moviemode != MM_GIF) + { + return; + } + + static tic_t oldtic = 0; + + // limit the recording to TICRATE + if (oldtic == I_GetTime()) + { + return; + } + + oldtic = I_GetTime(); + + GIF_frame_rgb24(width, height, reinterpret_cast(data.data())); +} + +static void M_SaveFrame_AVRecorder(uint32_t width, uint32_t height, tcb::span data) +{ +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES + if (M_AVRecorder_IsExpired()) + { + M_StopMovie(); + return; + } + + auto frame = g_av_recorder->new_staging_video_frame(width, height); + if (!frame) + { + // Not time to submit a frame! + return; + } + + auto data_begin = reinterpret_cast(data.data()); + auto data_end = reinterpret_cast(data.data() + data.size_bytes()); + std::copy(data_begin, data_end, frame->screen.begin()); + g_av_recorder->push_staging_video_frame(std::move(frame)); +#endif +} + +void M_SaveFrame(uint32_t width, uint32_t height, tcb::span data) +{ + switch (moviemode) + { + case MM_GIF: + M_SaveFrame_GIF(width, height, data); + break; + case MM_AVRECORDER: + M_SaveFrame_AVRecorder(width, height, data); + break; + default: + break; + } +} + void M_StopMovie(void) { #if NUMSCREENS > 2 @@ -1484,9 +1568,11 @@ void M_StopMovie(void) #endif case MM_SCREENSHOT: break; +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES case MM_AVRECORDER: M_AVRecorder_Close(); break; +#endif default: return; } @@ -1508,7 +1594,7 @@ void M_StopMovie(void) * \param palette Palette of image data. * \note if palette is NULL, BGR888 format */ -boolean M_SavePNG(const char *filename, void *data, int width, int height, const UINT8 *palette) +boolean M_SavePNG(const char *filename, const void *data, int width, int height, const UINT8 *palette) { png_structp png_ptr; png_infop png_info_ptr; @@ -1579,7 +1665,7 @@ boolean M_SavePNG(const char *filename, void *data, int width, int height, const png_write_info(png_ptr, png_info_ptr); - M_PNGImage(png_ptr, png_info_ptr, height, data); + M_PNGImage(png_ptr, png_info_ptr, height, (png_bytep)data); png_write_end(png_ptr, png_info_ptr); png_destroy_write_struct(&png_ptr, &png_info_ptr); @@ -1687,19 +1773,24 @@ void M_ScreenShot(void) takescreenshot = true; } +void M_DoLegacyGLScreenShot(void) +{ + const std::byte* fake_data = nullptr; + M_DoScreenShot(vid.width, vid.height, tcb::span(fake_data, vid.width * vid.height)); +} + /** Takes a screenshot. * The screenshot is saved as "srb2xxxx.png" where xxxx is the lowest * four-digit number for which a file does not already exist. * * \sa HWR_ScreenShot */ -void M_DoScreenShot(void) +void M_DoScreenShot(UINT32 width, UINT32 height, tcb::span data) { #if NUMSCREENS > 2 const char *freename = NULL; char pathname[MAX_WADPATH]; boolean ret = false; - UINT8 *linear = NULL; // Don't take multiple screenshots, obviously takescreenshot = false; @@ -1719,7 +1810,7 @@ void M_DoScreenShot(void) if (cv_screenshot_option.value != 3) { - strcat(pathname, PATHSEP"media"PATHSEP"screenshots"PATHSEP); + strcat(pathname, PATHSEP "media" PATHSEP "screenshots" PATHSEP); M_MkdirEach(pathname, M_PathParts(pathname) - 2, 0755); } @@ -1732,13 +1823,6 @@ void M_DoScreenShot(void) freename = Newsnapshotfile(pathname,"tga"); #endif - if (rendermode == render_soft) - { - // munge planar buffer to linear - linear = screens[2]; - I_ReadScreen(linear); - } - if (!freename) goto failure; @@ -1749,9 +1833,9 @@ void M_DoScreenShot(void) else #endif { - M_CreateScreenShotPalette(); + const void* pixel_data = static_cast(data.data()); #ifdef USE_PNG - ret = M_SavePNG(va(pandf,pathname,freename), linear, vid.width, vid.height, screenshot_palette); + ret = M_SavePNG(va(pandf,pathname,freename), pixel_data, width, height, NULL); #else ret = WritePCXfile(va(pandf,pathname,freename), linear, vid.width, vid.height, screenshot_palette); #endif @@ -2462,462 +2546,38 @@ TMatrix *RotateZMatrix(angle_t rad) char *sizeu1(size_t num) { static char sizeu1_buf[28]; - sprintf(sizeu1_buf, "%"PRIdS, num); + sprintf(sizeu1_buf, "%" PRIdS, num); return sizeu1_buf; } char *sizeu2(size_t num) { static char sizeu2_buf[28]; - sprintf(sizeu2_buf, "%"PRIdS, num); + sprintf(sizeu2_buf, "%" PRIdS, num); return sizeu2_buf; } char *sizeu3(size_t num) { static char sizeu3_buf[28]; - sprintf(sizeu3_buf, "%"PRIdS, num); + sprintf(sizeu3_buf, "%" PRIdS, num); return sizeu3_buf; } char *sizeu4(size_t num) { static char sizeu4_buf[28]; - sprintf(sizeu4_buf, "%"PRIdS, num); + sprintf(sizeu4_buf, "%" PRIdS, num); return sizeu4_buf; } char *sizeu5(size_t num) { static char sizeu5_buf[28]; - sprintf(sizeu5_buf, "%"PRIdS, num); + sprintf(sizeu5_buf, "%" PRIdS, num); return sizeu5_buf; } -#if defined (__GNUC__) && defined (__i386__) // from libkwave, under GPL -// Alam: note libkwave memcpy code comes from mplayer's libvo/aclib_template.c, r699 - -/* for small memory blocks (<256 bytes) this version is faster */ -#define small_memcpy(dest,src,n)\ -{\ -register unsigned long int dummy;\ -__asm__ __volatile__(\ - "cld\n\t"\ - "rep; movsb"\ - :"=&D"(dest), "=&S"(src), "=&c"(dummy)\ - :"0" (dest), "1" (src),"2" (n)\ - : "memory", "cc");\ -} -/* linux kernel __memcpy (from: /include/asm/string.h) */ -ATTRINLINE static FUNCINLINE void *__memcpy (void *dest, const void * src, size_t n) -{ - int d0, d1, d2; - - if ( n < 4 ) - { - small_memcpy(dest, src, n); - } - else - { - __asm__ __volatile__ ( - "rep ; movsl;" - "testb $2,%b4;" - "je 1f;" - "movsw;" - "1:\ttestb $1,%b4;" - "je 2f;" - "movsb;" - "2:" - : "=&c" (d0), "=&D" (d1), "=&S" (d2) - :"0" (n/4), "q" (n),"1" ((long) dest),"2" ((long) src) - : "memory"); - } - - return dest; -} - -#define SSE_MMREG_SIZE 16 -#define MMX_MMREG_SIZE 8 - -#define MMX1_MIN_LEN 0x800 /* 2K blocks */ -#define MIN_LEN 0x40 /* 64-byte blocks */ - -/* SSE note: i tried to move 128 bytes a time instead of 64 but it -didn't make any measureable difference. i'm using 64 for the sake of -simplicity. [MF] */ -static /*FUNCTARGET("sse2")*/ void *sse_cpy(void * dest, const void * src, size_t n) -{ - void *retval = dest; - size_t i; - - /* PREFETCH has effect even for MOVSB instruction ;) */ - __asm__ __volatile__ ( - "prefetchnta (%0);" - "prefetchnta 32(%0);" - "prefetchnta 64(%0);" - "prefetchnta 96(%0);" - "prefetchnta 128(%0);" - "prefetchnta 160(%0);" - "prefetchnta 192(%0);" - "prefetchnta 224(%0);" - "prefetchnta 256(%0);" - "prefetchnta 288(%0);" - : : "r" (src) ); - - if (n >= MIN_LEN) - { - register unsigned long int delta; - /* Align destinition to MMREG_SIZE -boundary */ - delta = ((unsigned long int)dest)&(SSE_MMREG_SIZE-1); - if (delta) - { - delta=SSE_MMREG_SIZE-delta; - n -= delta; - small_memcpy(dest, src, delta); - } - i = n >> 6; /* n/64 */ - n&=63; - if (((unsigned long)src) & 15) - /* if SRC is misaligned */ - for (; i>0; i--) - { - __asm__ __volatile__ ( - "prefetchnta 320(%0);" - "prefetchnta 352(%0);" - "movups (%0), %%xmm0;" - "movups 16(%0), %%xmm1;" - "movups 32(%0), %%xmm2;" - "movups 48(%0), %%xmm3;" - "movntps %%xmm0, (%1);" - "movntps %%xmm1, 16(%1);" - "movntps %%xmm2, 32(%1);" - "movntps %%xmm3, 48(%1);" - :: "r" (src), "r" (dest) : "memory"); - src = (const unsigned char *)src + 64; - dest = (unsigned char *)dest + 64; - } - else - /* - Only if SRC is aligned on 16-byte boundary. - It allows to use movaps instead of movups, which required data - to be aligned or a general-protection exception (#GP) is generated. - */ - for (; i>0; i--) - { - __asm__ __volatile__ ( - "prefetchnta 320(%0);" - "prefetchnta 352(%0);" - "movaps (%0), %%xmm0;" - "movaps 16(%0), %%xmm1;" - "movaps 32(%0), %%xmm2;" - "movaps 48(%0), %%xmm3;" - "movntps %%xmm0, (%1);" - "movntps %%xmm1, 16(%1);" - "movntps %%xmm2, 32(%1);" - "movntps %%xmm3, 48(%1);" - :: "r" (src), "r" (dest) : "memory"); - src = ((const unsigned char *)src) + 64; - dest = ((unsigned char *)dest) + 64; - } - /* since movntq is weakly-ordered, a "sfence" - * is needed to become ordered again. */ - __asm__ __volatile__ ("sfence":::"memory"); - /* enables to use FPU */ - __asm__ __volatile__ ("emms":::"memory"); - } - /* - * Now do the tail of the block - */ - if (n) __memcpy(dest, src, n); - return retval; -} - -static FUNCTARGET("mmx") void *mmx2_cpy(void *dest, const void *src, size_t n) -{ - void *retval = dest; - size_t i; - - /* PREFETCH has effect even for MOVSB instruction ;) */ - __asm__ __volatile__ ( - "prefetchnta (%0);" - "prefetchnta 32(%0);" - "prefetchnta 64(%0);" - "prefetchnta 96(%0);" - "prefetchnta 128(%0);" - "prefetchnta 160(%0);" - "prefetchnta 192(%0);" - "prefetchnta 224(%0);" - "prefetchnta 256(%0);" - "prefetchnta 288(%0);" - : : "r" (src)); - - if (n >= MIN_LEN) - { - register unsigned long int delta; - /* Align destinition to MMREG_SIZE -boundary */ - delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1); - if (delta) - { - delta=MMX_MMREG_SIZE-delta; - n -= delta; - small_memcpy(dest, src, delta); - } - i = n >> 6; /* n/64 */ - n&=63; - for (; i>0; i--) - { - __asm__ __volatile__ ( - "prefetchnta 320(%0);" - "prefetchnta 352(%0);" - "movq (%0), %%mm0;" - "movq 8(%0), %%mm1;" - "movq 16(%0), %%mm2;" - "movq 24(%0), %%mm3;" - "movq 32(%0), %%mm4;" - "movq 40(%0), %%mm5;" - "movq 48(%0), %%mm6;" - "movq 56(%0), %%mm7;" - "movntq %%mm0, (%1);" - "movntq %%mm1, 8(%1);" - "movntq %%mm2, 16(%1);" - "movntq %%mm3, 24(%1);" - "movntq %%mm4, 32(%1);" - "movntq %%mm5, 40(%1);" - "movntq %%mm6, 48(%1);" - "movntq %%mm7, 56(%1);" - :: "r" (src), "r" (dest) : "memory"); - src = ((const unsigned char *)src) + 64; - dest = ((unsigned char *)dest) + 64; - } - /* since movntq is weakly-ordered, a "sfence" - * is needed to become ordered again. */ - __asm__ __volatile__ ("sfence":::"memory"); - __asm__ __volatile__ ("emms":::"memory"); - } - /* - * Now do the tail of the block - */ - if (n) __memcpy(dest, src, n); - return retval; -} - -static FUNCTARGET("mmx") void *mmx1_cpy(void *dest, const void *src, size_t n) //3DNOW -{ - void *retval = dest; - size_t i; - - /* PREFETCH has effect even for MOVSB instruction ;) */ - __asm__ __volatile__ ( - "prefetch (%0);" - "prefetch 32(%0);" - "prefetch 64(%0);" - "prefetch 96(%0);" - "prefetch 128(%0);" - "prefetch 160(%0);" - "prefetch 192(%0);" - "prefetch 224(%0);" - "prefetch 256(%0);" - "prefetch 288(%0);" - : : "r" (src)); - - if (n >= MMX1_MIN_LEN) - { - register unsigned long int delta; - /* Align destinition to MMREG_SIZE -boundary */ - delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1); - if (delta) - { - delta=MMX_MMREG_SIZE-delta; - n -= delta; - small_memcpy(dest, src, delta); - } - i = n >> 6; /* n/64 */ - n&=63; - for (; i>0; i--) - { - __asm__ __volatile__ ( - "prefetch 320(%0);" - "prefetch 352(%0);" - "movq (%0), %%mm0;" - "movq 8(%0), %%mm1;" - "movq 16(%0), %%mm2;" - "movq 24(%0), %%mm3;" - "movq 32(%0), %%mm4;" - "movq 40(%0), %%mm5;" - "movq 48(%0), %%mm6;" - "movq 56(%0), %%mm7;" - "movq %%mm0, (%1);" - "movq %%mm1, 8(%1);" - "movq %%mm2, 16(%1);" - "movq %%mm3, 24(%1);" - "movq %%mm4, 32(%1);" - "movq %%mm5, 40(%1);" - "movq %%mm6, 48(%1);" - "movq %%mm7, 56(%1);" - :: "r" (src), "r" (dest) : "memory"); - src = ((const unsigned char *)src) + 64; - dest = ((unsigned char *)dest) + 64; - } - __asm__ __volatile__ ("femms":::"memory"); // same as mmx_cpy() but with a femms - } - /* - * Now do the tail of the block - */ - if (n) __memcpy(dest, src, n); - return retval; -} -#endif - -// Alam: why? memcpy may be __cdecl/_System and our code may be not the same type -static void *cpu_cpy(void *dest, const void *src, size_t n) -{ - if (src == NULL) - { - CONS_Debug(DBG_MEMORY, "Memcpy from 0x0?!: %p %p %s\n", dest, src, sizeu1(n)); - return dest; - } - - if(dest == NULL) - { - CONS_Debug(DBG_MEMORY, "Memcpy to 0x0?!: %p %p %s\n", dest, src, sizeu1(n)); - return dest; - } - - return memcpy(dest, src, n); -} - -static /*FUNCTARGET("mmx")*/ void *mmx_cpy(void *dest, const void *src, size_t n) -{ -#if defined (_MSC_VER) && defined (_X86_) - _asm - { - mov ecx, [n] - mov esi, [src] - mov edi, [dest] - shr ecx, 6 // mit mmx: 64bytes per iteration - jz lower_64 // if lower than 64 bytes - loop_64: // MMX transfers multiples of 64bytes - movq mm0, 0[ESI] // read sources - movq mm1, 8[ESI] - movq mm2, 16[ESI] - movq mm3, 24[ESI] - movq mm4, 32[ESI] - movq mm5, 40[ESI] - movq mm6, 48[ESI] - movq mm7, 56[ESI] - - movq 0[EDI], mm0 // write destination - movq 8[EDI], mm1 - movq 16[EDI], mm2 - movq 24[EDI], mm3 - movq 32[EDI], mm4 - movq 40[EDI], mm5 - movq 48[EDI], mm6 - movq 56[EDI], mm7 - - add esi, 64 - add edi, 64 - dec ecx - jnz loop_64 - emms // close mmx operation - lower_64:// transfer rest of buffer - mov ebx,esi - sub ebx,src - mov ecx,[n] - sub ecx,ebx - shr ecx, 3 // multiples of 8 bytes - jz lower_8 - loop_8: - movq mm0, [esi] // read source - movq [edi], mm0 // write destination - add esi, 8 - add edi, 8 - dec ecx - jnz loop_8 - emms // close mmx operation - lower_8: - mov ebx,esi - sub ebx,src - mov ecx,[n] - sub ecx,ebx - rep movsb - mov eax, [dest] // return dest - } -#elif defined (__GNUC__) && defined (__i386__) - void *retval = dest; - size_t i; - - if (n >= MMX1_MIN_LEN) - { - register unsigned long int delta; - /* Align destinition to MMREG_SIZE -boundary */ - delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1); - if (delta) - { - delta=MMX_MMREG_SIZE-delta; - n -= delta; - small_memcpy(dest, src, delta); - } - i = n >> 6; /* n/64 */ - n&=63; - for (; i>0; i--) - { - __asm__ __volatile__ ( - "movq (%0), %%mm0;" - "movq 8(%0), %%mm1;" - "movq 16(%0), %%mm2;" - "movq 24(%0), %%mm3;" - "movq 32(%0), %%mm4;" - "movq 40(%0), %%mm5;" - "movq 48(%0), %%mm6;" - "movq 56(%0), %%mm7;" - "movq %%mm0, (%1);" - "movq %%mm1, 8(%1);" - "movq %%mm2, 16(%1);" - "movq %%mm3, 24(%1);" - "movq %%mm4, 32(%1);" - "movq %%mm5, 40(%1);" - "movq %%mm6, 48(%1);" - "movq %%mm7, 56(%1);" - :: "r" (src), "r" (dest) : "memory"); - src = ((const unsigned char *)src) + 64; - dest = ((unsigned char *)dest) + 64; - } - __asm__ __volatile__ ("emms":::"memory"); - } - /* - * Now do the tail of the block - */ - if (n) __memcpy(dest, src, n); - return retval; -#else - return cpu_cpy(dest, src, n); -#endif -} - -void *(*M_Memcpy)(void* dest, const void* src, size_t n) = cpu_cpy; - -/** Memcpy that uses MMX, 3DNow, MMXExt or even SSE - * Do not use on overlapped memory, use memmove for that - */ -void M_SetupMemcpy(void) -{ -#if defined (__GNUC__) && defined (__i386__) - if (R_SSE2) - M_Memcpy = sse_cpy; - else if (R_MMXExt) - M_Memcpy = mmx2_cpy; - else if (R_3DNow) - M_Memcpy = mmx1_cpy; - else -#endif - if (R_MMX) - M_Memcpy = mmx_cpy; -#if 0 - M_Memcpy = cpu_cpy; -#endif -} - /** Return the appropriate message for a file error or end of file. */ const char *M_FileError(FILE *fp) @@ -3026,7 +2686,7 @@ int M_JumpWord(const char *line) if (isspace(line[1])) return 1 + strspn(&line[1], " "); else - return strcspn(line, " "PUNCTUATION); + return strcspn(line, " " PUNCTUATION); } } diff --git a/src/m_misc.h b/src/m_misc.h index 2df52fa96..8012c442d 100644 --- a/src/m_misc.h +++ b/src/m_misc.h @@ -22,6 +22,14 @@ #include "command.h" #ifdef __cplusplus + +#include + +#include + +void M_DoScreenShot(uint32_t width, uint32_t height, tcb::span data); +void M_SaveFrame(uint32_t width, uint32_t height, tcb::span data); + extern "C" { #endif @@ -41,7 +49,7 @@ extern consvar_t cv_zlib_memorya, cv_zlib_levela, cv_zlib_strategya, cv_zlib_win extern consvar_t cv_apng_delay, cv_apng_downscale; void M_StartMovie(void); -void M_SaveFrame(void); +void M_LegacySaveFrame(void); void M_StopMovie(void); // the file where game vars and settings are saved @@ -82,12 +90,14 @@ void FIL_ForceExtension(char *path, const char *extension); boolean FIL_CheckExtension(const char *in); #ifdef HAVE_PNG -boolean M_SavePNG(const char *filename, void *data, int width, int height, const UINT8 *palette); +boolean M_SavePNG(const char *filename, const void *data, int width, int height, const UINT8 *palette); #endif extern boolean takescreenshot; void M_ScreenShot(void); -void M_DoScreenShot(void); +#ifdef HWRENDER +void M_DoLegacyGLScreenShot(void); +#endif boolean M_ScreenshotResponder(event_t *ev); void M_MinimapGenerate(void); diff --git a/src/media/avrecorder.hpp b/src/media/avrecorder.hpp index fdee91b18..3f2cf684a 100644 --- a/src/media/avrecorder.hpp +++ b/src/media/avrecorder.hpp @@ -58,17 +58,16 @@ public: }; // TODO: remove once hwr2 twodee is finished - struct IndexedVideoFrame + struct StagingVideoFrame { - using instance_t = std::unique_ptr; + using instance_t = std::unique_ptr; - std::array palette; std::vector 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_) + StagingVideoFrame(uint32_t width_, uint32_t height_, int pts_) : + screen(width_ * height_ * 3), width(width_), height(height_), pts(pts_) { } }; @@ -87,9 +86,9 @@ public: // 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); + StagingVideoFrame::instance_t new_staging_video_frame(uint32_t width, uint32_t height); - void push_indexed_video_frame(IndexedVideoFrame::instance_t frame); + void push_staging_video_frame(StagingVideoFrame::instance_t frame); // Proper name of the container format. const char* format_name() const; diff --git a/src/media/avrecorder_impl.hpp b/src/media/avrecorder_impl.hpp index 3fab89df8..bc4338932 100644 --- a/src/media/avrecorder_impl.hpp +++ b/src/media/avrecorder_impl.hpp @@ -53,7 +53,7 @@ public: template struct Traits { - using frame_type = IndexedVideoFrame::instance_t; + using frame_type = StagingVideoFrame::instance_t; }; std::vector::frame_type> vec_; @@ -151,8 +151,7 @@ private: 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); + VideoFrame::instance_t convert_staging_video_frame(const StagingVideoFrame& indexed); }; template <> diff --git a/src/media/avrecorder_indexed.cpp b/src/media/avrecorder_indexed.cpp index 31846b7da..f69008b85 100644 --- a/src/media/avrecorder_indexed.cpp +++ b/src/media/avrecorder_indexed.cpp @@ -21,34 +21,36 @@ using namespace srb2::media; using Impl = AVRecorder::Impl; -VideoFrame::instance_t Impl::convert_indexed_video_frame(const IndexedVideoFrame& indexed) +VideoFrame::instance_t Impl::convert_staging_video_frame(const StagingVideoFrame& staging) { - VideoFrame::instance_t frame = video_encoder_->new_frame(indexed.width, indexed.height, indexed.pts); + VideoFrame::instance_t frame = video_encoder_->new_frame(staging.width, staging.height, staging.pts); SRB2_ASSERT(frame != nullptr); const VideoFrame::Buffer& buffer = frame->rgba_buffer(); - const uint8_t* s = indexed.screen.data(); + const uint8_t* s = staging.screen.data(); uint8_t* p = buffer.plane.data(); + // Convert from RGB8 to RGBA8 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(p)[x] = c.rgba; + p[x * 4] = s[x * 3]; + p[x * 4 + 1] = s[x * 3 + 1]; + p[x * 4 + 2] = s[x * 3 + 2]; + p[x * 4 + 3] = 255; } - s += indexed.width; + s += staging.width * 3; p += buffer.row_stride; } return frame; } -AVRecorder::IndexedVideoFrame::instance_t AVRecorder::new_indexed_video_frame(uint32_t width, uint32_t height) +AVRecorder::StagingVideoFrame::instance_t AVRecorder::new_staging_video_frame(uint32_t width, uint32_t height) { std::optional pts = impl_->advance_video_pts(); @@ -57,10 +59,10 @@ AVRecorder::IndexedVideoFrame::instance_t AVRecorder::new_indexed_video_frame(ui return nullptr; } - return std::make_unique(width, height, *pts); + return std::make_unique(width, height, *pts); } -void AVRecorder::push_indexed_video_frame(IndexedVideoFrame::instance_t frame) +void AVRecorder::push_staging_video_frame(StagingVideoFrame::instance_t frame) { auto _ = impl_->queue_guard(); diff --git a/src/media/avrecorder_queue.cpp b/src/media/avrecorder_queue.cpp index 4a94e1abe..f18c7b8fd 100644 --- a/src/media/avrecorder_queue.cpp +++ b/src/media/avrecorder_queue.cpp @@ -125,7 +125,7 @@ Impl::QueueState Impl::encode_queues() { for (auto& p : copy) { - auto frame = convert_indexed_video_frame(*p); + auto frame = convert_staging_video_frame(*p); video_encoder_->encode(std::move(frame)); } diff --git a/src/menus/options-data-screenshots.c b/src/menus/options-data-screenshots.c index 809eb5e1b..0cb1d0762 100644 --- a/src/menus/options-data-screenshots.c +++ b/src/menus/options-data-screenshots.c @@ -3,7 +3,9 @@ #include "../k_menu.h" #include "../m_misc.h" // screenshot cvars +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #include "../m_avrecorder.h" +#endif menuitem_t OPTIONS_DataScreenshot[] = { @@ -26,15 +28,19 @@ menuitem_t OPTIONS_DataScreenshot[] = {IT_STRING | IT_CVAR, "Recording Format", "What file format will movies will be recorded in?", NULL, {.cvar = &cv_moviemode}, 0, 0}, +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES {IT_STRING | IT_CVAR, "Real-Time Data", "If enabled, shows fps, duration and filesize of recording in real-time.", NULL, {.cvar = &cv_movie_showfps}, 0, 0}, +#else + {IT_SPACE | IT_NOTHING, NULL, NULL, + NULL, {NULL}, 0, 0}, +#endif {IT_STRING | IT_CVAR, "Storage Location", "Sets where to store movies.", NULL, {.cvar = &cv_movie_option}, 0, 0}, {IT_STRING | IT_CVAR | IT_CV_STRING, "Custom Folder", "Specify which folder to save videos in.", NULL, {.cvar = &cv_movie_folder}, 24, 0}, - }; menu_t OPTIONS_DataScreenshotDef = { diff --git a/src/menus/play-local-race-time-attack.c b/src/menus/play-local-race-time-attack.c index 054681792..d41f29dca 100644 --- a/src/menus/play-local-race-time-attack.c +++ b/src/menus/play-local-race-time-attack.c @@ -446,7 +446,7 @@ void M_StartTimeAttack(INT32 choice) F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_level_toblack, wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); SV_StartSinglePlayerServer(levellist.newgametype, false); diff --git a/src/menus/transient/cup-select.c b/src/menus/transient/cup-select.c index 6484d45f5..23a85e4e0 100644 --- a/src/menus/transient/cup-select.c +++ b/src/menus/transient/cup-select.c @@ -112,7 +112,7 @@ void M_CupSelectHandler(INT32 choice) F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_level_toblack, wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); memset(&grandprixinfo, 0, sizeof(struct grandprixinfo)); diff --git a/src/menus/transient/level-select.c b/src/menus/transient/level-select.c index 55cfa3e80..461473bd9 100644 --- a/src/menus/transient/level-select.c +++ b/src/menus/transient/level-select.c @@ -484,7 +484,7 @@ void M_LevelSelected(INT16 add) F_WipeStartScreen(); V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_level_toblack, wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); SV_StartSinglePlayerServer(levellist.newgametype, levellist.netgame); diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 440f700de..ca8680205 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -12,4 +12,5 @@ target_sources(SRB2SDL2 PRIVATE ufo.c monitor.c item-spot.c + loops.c ) diff --git a/src/objects/loops.c b/src/objects/loops.c new file mode 100644 index 000000000..dd9ea7c3b --- /dev/null +++ b/src/objects/loops.c @@ -0,0 +1,281 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James R. +// Copyright (C) 2023 by Kart Krew +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file loop-endpoint.c +/// \brief Sonic loops, start and end points + +#include "../doomdef.h" +#include "../k_kart.h" +#include "../taglist.h" +#include "../p_local.h" +#include "../p_setup.h" +#include "../p_spec.h" +#include "../r_main.h" +#include "../k_objects.h" + +#define end_anchor(o) ((o)->target) + +#define center_max_revolution(o) ((o)->threshold) +#define center_alpha(o) ((o)->target) +#define center_beta(o) ((o)->tracer) + +static inline boolean +center_has_flip (const mobj_t *center) +{ + return (center->flags2 & MF2_AMBUSH) == MF2_AMBUSH; +} + +static inline void +center_set_flip +( mobj_t * center, + boolean mode) +{ + center->flags2 = (center->flags2 & ~(MF2_AMBUSH)) | + ((mode != false) * MF2_AMBUSH); +} + +#define anchor_center(o) ((o)->target) +#define anchor_other(o) ((o)->tracer) +#define anchor_type(o) ((o)->reactiontime) + +static void +set_shiftxy +( player_t * player, + const mobj_t * a) +{ + const mobj_t *b = anchor_other(a); + + const fixed_t dx = (b->x - a->x); + const fixed_t dy = (b->y - a->y); + + const angle_t th = + (R_PointToAngle2(0, 0, dx, dy) - a->angle); + + const fixed_t adj = FixedMul( + abs(FCOS(AbsAngle(th - ANGLE_90))), + FixedHypot(dx, dy)) / 2; + + vector2_t *xy = &player->loop.shift; + + xy->x = FixedMul(FSIN(a->angle), adj); + xy->y = FixedMul(FCOS(a->angle), adj); +} + +static void +measure_clock +( const mobj_t * center, + const mobj_t * anchor, + angle_t * pitch, + fixed_t * radius) +{ + const fixed_t dx = (anchor->x - center->x); + const fixed_t dy = (anchor->y - center->y); + const fixed_t dz = (anchor->z - center->z); + + /* Translate the anchor point to be along a center line. + This makes the horizontal position one dimensional + relative to the center point. */ + const fixed_t xy = ( + FixedMul(dx, FCOS(anchor->angle)) + + FixedMul(dy, FSIN(anchor->angle))); + + /* The 3d position of the anchor point is then reduced to + two axes and can be measured as an angle. */ + *pitch = R_PointToAngle2(0, 0, xy, dz) + ANGLE_90; + *radius = FixedHypot(xy, dz); +} + +static void +crisscross +( mobj_t * anchor, + mobj_t ** target_p, + mobj_t ** other_p) +{ + P_SetTarget(target_p, anchor); + + if (!P_MobjWasRemoved(*other_p)) + { + P_SetTarget(&anchor_other(anchor), *other_p); + P_SetTarget(&anchor_other(*other_p), anchor); + } +} + +static boolean +moving_toward_gate +( const player_t * player, + const mobj_t * anchor, + angle_t pitch) +{ + const fixed_t + x = player->mo->momx, + y = player->mo->momy, + z = player->mo->momz, + + zx = FixedMul(FCOS(anchor->angle), z), + zy = FixedMul(FSIN(anchor->angle), z), + + co = abs(FCOS(pitch)), + si = abs(FSIN(pitch)), + + dx = FixedMul(co, x) + FixedMul(si, zx), + dy = FixedMul(co, y) + FixedMul(si, zy); + + return AngleDelta(anchor->angle, + R_PointToAngle2(0, 0, dx, dy)) < ANG60; +} + +static SINT8 +get_binary_direction +( angle_t pitch, + mobj_t * toucher) +{ + const fixed_t si = FSIN(pitch); + + if (abs(si) < abs(FCOS(pitch))) + { + // pitch = 0 points downward so offset 90 degrees + // clockwise so 180 occurs at horizon + return ((pitch + ANGLE_90) < ANGLE_180) ? 1 : -(1); + } + else + { + return intsign(si) * P_MobjFlip(toucher); + } +} + +mobj_t * +Obj_FindLoopCenter (const mtag_t tag) +{ + INT32 i; + + TAG_ITER_THINGS(tag, i) + { + mapthing_t *mt = &mapthings[i]; + + if (mt->type == mobjinfo[MT_LOOPCENTERPOINT].doomednum) + { + return mt->mobj; + } + } + + return NULL; +} + +void +Obj_InitLoopEndpoint +( mobj_t * end, + mobj_t * anchor) +{ + P_SetTarget(&end_anchor(end), anchor); +} + +void +Obj_InitLoopCenter (mobj_t *center) +{ + const mapthing_t *mt = center->spawnpoint; + + center_max_revolution(center) = mt->args[1] * FRACUNIT / 360; + center_set_flip(center, mt->args[0]); +} + +void +Obj_LinkLoopAnchor +( mobj_t * anchor, + mobj_t * center, + UINT8 type) +{ + P_SetTarget(&anchor_center(anchor), center); + + anchor_type(anchor) = type; + + if (!P_MobjWasRemoved(center)) + { + switch (type) + { + case TMLOOP_ALPHA: + crisscross(anchor, + ¢er_alpha(center), + ¢er_beta(center)); + break; + + case TMLOOP_BETA: + crisscross(anchor, + ¢er_beta(center), + ¢er_alpha(center)); + break; + } + } +} + +void +Obj_LoopEndpointCollide +( mobj_t * end, + mobj_t * toucher) +{ + player_t *player = toucher->player; + sonicloopvars_t *s = &player->loop; + + mobj_t *anchor = end_anchor(end); + mobj_t *center = anchor ? anchor_center(anchor) : NULL; + + angle_t pitch; + fixed_t radius; + + /* predict movement for a smooth transition */ + const fixed_t px = toucher->x + toucher->momx; + const fixed_t py = toucher->y + toucher->momy; + + SINT8 flip; + + if (P_MobjWasRemoved(center)) + { + return; + } + + if (player->loop.radius != 0) + { + return; + } + + measure_clock(center, anchor, &pitch, &radius); + + if (!moving_toward_gate(player, anchor, pitch)) + { + return; + } + + if (!P_MobjWasRemoved(anchor_other(anchor))) + { + set_shiftxy(player, anchor); + } + + flip = get_binary_direction(pitch, toucher); + + s->yaw = anchor->angle; + + s->origin.x = center->x - (anchor->x - px); + s->origin.y = center->y - (anchor->y - py); + s->origin.z = center->z; + + s->radius = radius * flip; + s->revolution = AngleFixed(pitch) / 360; + + s->min_revolution = s->revolution; + s->max_revolution = s->revolution + + center_max_revolution(center) * flip; + + s->flip = center_has_flip(center); + + player->speed = + 3 * (player->speed + toucher->momz) / 2; + + /* cancel the effects of K_Squish */ + toucher->spritexscale = FRACUNIT; + toucher->spriteyscale = FRACUNIT; +} diff --git a/src/objects/monitor.c b/src/objects/monitor.c index 50d13ee3c..8f5be65eb 100644 --- a/src/objects/monitor.c +++ b/src/objects/monitor.c @@ -602,6 +602,12 @@ Obj_MonitorGetDamage damage = HEALTHFACTOR + (FixedMul(weight, HEALTHFACTOR) / 9); + + if (inflictor->player->tiregrease > 0) + { + damage *= 3; // Do 3x the damage if the player is in spring grease state + } + if (inflictor->scale > mapobjectscale) { damage = P_ScaleFromMap(damage, inflictor->scale); diff --git a/src/objects/orbinaut.c b/src/objects/orbinaut.c index 27e3fe742..6f9ba3788 100644 --- a/src/objects/orbinaut.c +++ b/src/objects/orbinaut.c @@ -335,23 +335,24 @@ void Obj_OrbinautJawzMoveHeld(player_t *player) { fixed_t finalscale = K_ItemScaleForPlayer(player); fixed_t speed = 0; - mobj_t *cur = NULL; + mobj_t *cur = NULL, *next = player->mo->hnext; player->bananadrag = 0; // Just to make sure - cur = player->mo->hnext; - speed = ((8 - min(4, player->itemamount)) * cur->info->speed) / 7; + if (next == NULL) + return; - while (cur != NULL && P_MobjWasRemoved(cur) == false) + speed = ((8 - min(4, player->itemamount)) * next->info->speed) / 7; + + while ((cur = next) != NULL && P_MobjWasRemoved(cur) == false) { const fixed_t radius = FixedHypot(player->mo->radius, player->mo->radius) + FixedHypot(cur->radius, cur->radius); // mobj's distance from its Target, or Radius. fixed_t z; + next = cur->hnext; + if (!cur->health) - { - cur = cur->hnext; continue; - } cur->color = player->skincolor; @@ -391,15 +392,17 @@ void Obj_OrbinautJawzMoveHeld(player_t *player) z = player->mo->z + player->mo->height - cur->height; } - cur->flags |= MF_NOCLIPTHING; // temporarily make them noclip other objects so they can't hit anyone while in the player + cur->flags |= (MF_NOCLIP|MF_NOCLIPTHING); // temporarily make them noclip other objects so they can't hit anyone while in the player P_MoveOrigin(cur, player->mo->x, player->mo->y, z); cur->momx = FixedMul(FINECOSINE(cur->angle >> ANGLETOFINESHIFT), orbinaut_shield_dist(cur)); cur->momy = FixedMul(FINESINE(cur->angle >> ANGLETOFINESHIFT), orbinaut_shield_dist(cur)); - cur->flags &= ~MF_NOCLIPTHING; + cur->flags &= ~(MF_NOCLIP|MF_NOCLIPTHING); if (!P_TryMove(cur, player->mo->x + cur->momx, player->mo->y + cur->momy, true, NULL)) { P_SlideMove(cur, NULL); + if (P_MobjWasRemoved(cur)) + continue; } if (P_IsObjectOnGround(player->mo)) @@ -426,8 +429,6 @@ void Obj_OrbinautJawzMoveHeld(player_t *player) cur->z = z; cur->momx = cur->momy = 0; cur->angle += ANGLE_90; - - cur = cur->hnext; } } diff --git a/src/p_inter.c b/src/p_inter.c index 6e053a405..3d69eec25 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -601,6 +601,10 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) } return; + case MT_LOOPENDPOINT: + Obj_LoopEndpointCollide(special, toucher); + return; + default: // SOC or script pickup P_SetTarget(&special->target, toucher); break; @@ -1911,6 +1915,12 @@ static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, { (void)source; + if (player->respawn.state != RESPAWNST_NONE) + { + K_DoInstashield(player); + return false; + } + if (!player->exiting && specialstageinfo.valid == true) { player->pflags |= PF_NOCONTEST; diff --git a/src/p_local.h b/src/p_local.h index bf847fbca..12362a548 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -193,6 +193,10 @@ boolean P_AutoPause(void); void P_ElementalFire(player_t *player, boolean cropcircle); void P_SpawnSkidDust(player_t *player, fixed_t radius, boolean sound); +void P_HaltPlayerOrbit(player_t *player); +void P_ExitPlayerOrbit(player_t *player); +boolean P_PlayerOrbit(player_t *player); + void P_MovePlayer(player_t *player); void P_PlayerThink(player_t *player); void P_PlayerAfterThink(player_t *player); diff --git a/src/p_loop.c b/src/p_loop.c new file mode 100644 index 000000000..e2d838f31 --- /dev/null +++ b/src/p_loop.c @@ -0,0 +1,180 @@ +// SONIC ROBO BLAST 2 KART +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Kart Krew +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file p_loop.c +/// \brief Sonic loop physics + +#include "doomdef.h" +#include "d_player.h" +#include "k_kart.h" +#include "p_local.h" +#include "p_setup.h" +#include "p_slopes.h" +#include "r_main.h" + +static inline angle_t +get_pitch (fixed_t revolution) +{ + return FixedAngle((revolution & FRACMASK) * 360); +} + +static inline fixed_t +get_shift_curve (const sonicloopvars_t *s) +{ + const angle_t th = get_pitch(FixedDiv( + s->revolution - s->min_revolution, + s->max_revolution - s->min_revolution)); + + // XY shift is transformed on wave scale; less movement + // at start and end of rotation, more halfway. + return FSIN((th / 2) - ANGLE_90); +} + +void P_HaltPlayerOrbit(player_t *player) +{ + // see P_PlayerOrbit + player->mo->flags &= ~(MF_NOCLIPHEIGHT); + + player->loop.radius = 0; +} + +void P_ExitPlayerOrbit(player_t *player) +{ + sonicloopvars_t *s = &player->loop; + + angle_t pitch = get_pitch(s->revolution); + angle_t yaw = s->yaw; + + fixed_t co, si; + fixed_t speed; + + if (s->radius < 0) + { + pitch += ANGLE_180; + } + + co = FCOS(pitch); + si = FSIN(pitch); + + speed = FixedMul(co, player->speed); + + P_InstaThrust(player->mo, yaw, speed); + + player->mo->momz = FixedMul(si, player->speed); + + if (speed < 0) + { + yaw += ANGLE_180; + } + + // excludes only extremely vertical angles + if (abs(co) * 4 > abs(si)) + { + P_SetPlayerAngle(player, yaw); + } + + if (s->flip) + { + player->mo->eflags ^= MFE_VERTICALFLIP; + player->mo->flags2 ^= MF2_OBJECTFLIP; + + P_SetPitchRoll(player->mo, + pitch + ANGLE_180, s->yaw); + } + + // tiregrease gives less friction, extends momentum + player->tiregrease = TICRATE; + + P_HaltPlayerOrbit(player); +} + +boolean P_PlayerOrbit(player_t *player) +{ + sonicloopvars_t *s = &player->loop; + + angle_t pitch; + + fixed_t xy, z; + fixed_t xs, ys; + + fixed_t step, th, left; + + fixed_t grav; + + if (s->radius == 0) + { + return false; + } + + grav = abs(P_GetMobjGravity(player->mo)); + + // Lose speed on the way up. revolution = 0.5 always + // points straight up. + if (abs(s->revolution & FRACMASK) < FRACUNIT/2) + { + player->speed -= grav; + } + else + { + player->speed += 4 * grav; + } + + pitch = get_pitch(s->revolution); + + xy = FixedMul(abs(s->radius), FSIN(pitch)); + z = FixedMul(abs(s->radius), -(FCOS(pitch))); + + th = get_shift_curve(s); + + xs = FixedMul(s->shift.x, th); + ys = FixedMul(s->shift.y, th); + + xs += FixedMul(xy, FCOS(s->yaw)); + ys += FixedMul(xy, FSIN(s->yaw)); + + P_MoveOrigin(player->mo, + s->origin.x + xs, + s->origin.y + ys, + s->origin.z + z); + + // Match rollangle to revolution + P_SetPitchRoll(player->mo, + s->radius < 0 ? (ANGLE_180 + pitch) : pitch, + s->yaw); + + // circumfrence = (2r)PI + step = FixedDiv(player->speed, + FixedMul(s->radius, M_TAU_FIXED)); + + left = (s->max_revolution - s->revolution); + + if (abs(left) < abs(step)) + { + P_ExitPlayerOrbit(player); + + return false; + } + + // If player slows down by too much, throw them out of + // the loop in a tumble. + if (player->speed < player->mo->scale) + { + P_HaltPlayerOrbit(player); + K_StumblePlayer(player); + + return false; + } + + s->revolution += step; + + // We need to not clip the ground. It sucks but setting + // this flag is the only way to do that. + player->mo->flags |= (MF_NOCLIPHEIGHT); + + return true; +} diff --git a/src/p_map.c b/src/p_map.c index 1f7051c10..a02a5191d 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -917,6 +917,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) || tm.thing->type == MT_BANANA || tm.thing->type == MT_EGGMANITEM || tm.thing->type == MT_BALLHOG || tm.thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || tm.thing->type == MT_SINK || tm.thing->type == MT_GARDENTOP + || tm.thing->type == MT_DROPTARGET || (tm.thing->type == MT_PLAYER && thing->target != tm.thing))) { // see if it went over / under @@ -931,8 +932,9 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) || (tm.thing->player && tm.thing->player->bubbleblowup)) && (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_GACHABOM || thing->type == MT_BANANA || thing->type == MT_EGGMANITEM || thing->type == MT_BALLHOG - || thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || thing->type == MT_SINK + || thing->type == MT_SSMINE || thing->type == MT_LANDMINE || thing->type == MT_SINK || thing->type == MT_GARDENTOP + || thing->type == MT_DROPTARGET || (thing->type == MT_PLAYER && tm.thing->target != thing))) { // see if it went over / under @@ -967,7 +969,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) else if ((tm.thing->type == MT_DROPTARGET || tm.thing->type == MT_DROPTARGET_SHIELD) && (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_GACHABOM || thing->type == MT_BANANA || thing->type == MT_EGGMANITEM || thing->type == MT_BALLHOG - || thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || thing->type == MT_SINK + || thing->type == MT_SSMINE || thing->type == MT_LANDMINE || thing->type == MT_SINK || thing->type == MT_GARDENTOP || (thing->type == MT_PLAYER))) { @@ -2674,6 +2676,22 @@ fixed_t P_GetThingStepUp(mobj_t *thing, fixed_t destX, fixed_t destY) return maxstep; } +static boolean P_UsingStepUp(mobj_t *thing) +{ + if (thing->flags & MF_NOCLIP) + { + return false; + } + + // orbits have no collision + if (thing->player && thing->player->loop.radius) + { + return false; + } + + return true; +} + static boolean increment_move ( mobj_t * thing, @@ -2734,7 +2752,7 @@ increment_move // copy into the spechitint buffer from spechit spechitint_copyinto(); - if (!(thing->flags & MF_NOCLIP)) + if (P_UsingStepUp(thing)) { // All things are affected by their scale. fixed_t maxstep = P_GetThingStepUp(thing, tryx, tryy); diff --git a/src/p_mobj.c b/src/p_mobj.c index 52e48da51..3f0481a94 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -46,6 +46,7 @@ #include "k_collide.h" #include "k_objects.h" #include "k_grandprix.h" +#include "k_director.h" static CV_PossibleValue_t CV_BobSpeed[] = {{0, "MIN"}, {4*FRACUNIT, "MAX"}, {0, NULL}}; consvar_t cv_movebob = CVAR_INIT ("movebob", "1.0", CV_FLOAT|CV_SAVE, CV_BobSpeed, NULL); @@ -1790,6 +1791,8 @@ void P_XYMovement(mobj_t *mo) { S_StartSound(mo, mo->info->attacksound); mo->health--; + // This prevents an item thrown at a wall from + // phasing through you on its return. mo->threshold = 0; } /*FALLTHRU*/ @@ -1810,6 +1813,12 @@ void P_XYMovement(mobj_t *mo) S_StartSound(mo, sfx_s3k44); // Bubble bounce break; + case MT_DROPTARGET: + // This prevents an item thrown at a wall from + // phasing through you on its return. + mo->threshold = 0; + break; + default: break; } @@ -3978,7 +3987,8 @@ static void P_CheckFloatbobPlatforms(mobj_t *mobj) static void P_SquishThink(mobj_t *mobj) { if (!(mobj->flags & MF_NOSQUISH) && - !(mobj->eflags & MFE_SLOPELAUNCHED)) + !(mobj->eflags & MFE_SLOPELAUNCHED) && + !(mobj->player && mobj->player->loop.radius != 0)) { K_Squish(mobj); } @@ -4001,7 +4011,8 @@ static void P_PlayerMobjThinker(mobj_t *mobj) // Zoom tube if ((mobj->player->carry == CR_ZOOMTUBE && mobj->tracer && !P_MobjWasRemoved(mobj->tracer)) - || mobj->player->respawn.state == RESPAWNST_MOVE) + || mobj->player->respawn.state == RESPAWNST_MOVE + || mobj->player->loop.radius != 0) { P_HitSpecialLines(mobj, mobj->x, mobj->y, mobj->momx, mobj->momy); P_UnsetThingPosition(mobj); @@ -9742,14 +9753,6 @@ void P_MobjThinker(mobj_t *mobj) I_Assert(mobj != NULL); I_Assert(!P_MobjWasRemoved(mobj)); - // Set old position (for interpolation) - mobj->old_x = mobj->x; - mobj->old_y = mobj->y; - mobj->old_z = mobj->z; - mobj->old_angle = mobj->angle; - mobj->old_pitch = mobj->pitch; - mobj->old_roll = mobj->roll; - // Remove dead target/tracer. if (mobj->target && P_MobjWasRemoved(mobj->target)) P_SetTarget(&mobj->target, NULL); @@ -11827,6 +11830,22 @@ void P_SpawnPlayer(INT32 playernum) K_SpawnPlayerBattleBumpers(p); } } + + // I'm not refactoring the loop at the top of this file. + pcount = 0; + + for (i = 0; i < MAXPLAYERS; ++i) + { + if (G_CouldView(i)) + { + pcount++; + } + } + + // Spectating when there is literally any other player in + // the level enables director cam. + // TODO: how do we support splitscreen? + K_ToggleDirector(players[consoleplayer].spectator && pcount > 0); } void P_AfterPlayerSpawn(INT32 playernum) @@ -13316,6 +13335,11 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean mobj->reactiontime++; break; } + case MT_LOOPCENTERPOINT: + { + Obj_InitLoopCenter(mobj); + break; + } default: break; } @@ -13536,6 +13560,11 @@ static void P_SpawnItemRow(mapthing_t *mthing, mobjtype_t *itemtypes, UINT8 numi angle_t angle = FixedAngle(fixedangle << FRACBITS); angle_t fineangle = (angle >> ANGLETOFINESHIFT) & FINEMASK; + boolean isloopend = (mthing->type == mobjinfo[MT_LOOPENDPOINT].doomednum); + mobj_t *loopanchor; + + boolean inclusive = isloopend; + for (r = 0; r < numitemtypes; r++) { dummything = *mthing; @@ -13554,6 +13583,21 @@ static void P_SpawnItemRow(mapthing_t *mthing, mobjtype_t *itemtypes, UINT8 numi } z = P_GetMobjSpawnHeight(itemtypes[0], x, y, z, 0, mthing->options & MTF_OBJECTFLIP, mthing->scale); + if (isloopend) + { + const fixed_t length = (numitems - 1) * horizontalspacing / 2; + + mobj_t *loopcenter = Obj_FindLoopCenter(Tag_FGet(&mthing->tags)); + + // Spawn the anchor at the middle point of the line + loopanchor = P_SpawnMobjFromMapThing(&dummything, + x + FixedMul(length, FINECOSINE(fineangle)), + y + FixedMul(length, FINESINE(fineangle)), + z, MT_LOOPCENTERPOINT); + + Obj_LinkLoopAnchor(loopanchor, loopcenter, mthing->args[0]); + } + for (r = 0; r < numitems; r++) { mobjtype_t itemtype = itemtypes[r % numitemtypes]; @@ -13561,15 +13605,24 @@ static void P_SpawnItemRow(mapthing_t *mthing, mobjtype_t *itemtypes, UINT8 numi continue; dummything.type = mobjinfo[itemtype].doomednum; + if (inclusive) + mobj = P_SpawnMobjFromMapThing(&dummything, x, y, z, itemtype); + x += FixedMul(horizontalspacing, FINECOSINE(fineangle)); y += FixedMul(horizontalspacing, FINESINE(fineangle)); z += (mthing->options & MTF_OBJECTFLIP) ? -verticalspacing : verticalspacing; - mobj = P_SpawnMobjFromMapThing(&dummything, x, y, z, itemtype); + if (!inclusive) + mobj = P_SpawnMobjFromMapThing(&dummything, x, y, z, itemtype); if (!mobj) continue; + if (isloopend) + { + Obj_InitLoopEndpoint(mobj, loopanchor); + } + mobj->spawnpoint = NULL; } } @@ -13717,6 +13770,18 @@ void P_SpawnItemPattern(mapthing_t *mthing) } } +void P_SpawnItemLine(mapthing_t *mt1, mapthing_t *mt2) +{ + const mobjtype_t type = P_GetMobjtype(mt1->type); + const fixed_t diameter = 2 * FixedMul(mobjinfo[type].radius, mapobjectscale); + const fixed_t dx = (mt2->x - mt1->x) * FRACUNIT; + const fixed_t dy = (mt2->y - mt1->y) * FRACUNIT; + const fixed_t dist = FixedHypot(dx, dy); + const angle_t angle = R_PointToAngle2(0, 0, dx, dy); + + P_SpawnSingularItemRow(mt1, type, (dist / diameter) + 1, diameter, 0, AngleFixed(angle) / FRACUNIT); +} + // // P_CheckMissileSpawn // Moves the missile forward a bit and possibly explodes it right there. diff --git a/src/p_mobj.h b/src/p_mobj.h index dd448841e..d5dc67a3e 100644 --- a/src/p_mobj.h +++ b/src/p_mobj.h @@ -530,6 +530,7 @@ fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mt mobj_t *P_SpawnMapThing(mapthing_t *mthing); void P_SpawnHoop(mapthing_t *mthing); void P_SpawnItemPattern(mapthing_t *mthing); +void P_SpawnItemLine(mapthing_t *mt1, mapthing_t *mt2); void P_SpawnHoopOfSomething(fixed_t x, fixed_t y, fixed_t z, fixed_t radius, INT32 number, mobjtype_t type, angle_t rotangle); void P_SpawnPrecipitation(void); void P_SpawnParaloop(fixed_t x, fixed_t y, fixed_t z, fixed_t radius, INT32 number, mobjtype_t type, statenum_t nstate, angle_t rotangle, boolean spawncenter); diff --git a/src/p_saveg.c b/src/p_saveg.c index 55fac75f5..fc9d7daf4 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -465,6 +465,19 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT32(save->p, players[i].itemRoulette.tics); WRITEUINT32(save->p, players[i].itemRoulette.elapsed); WRITEUINT8(save->p, players[i].itemRoulette.eggman); + + // sonicloopsvars_t + WRITEFIXED(save->p, players[i].loop.radius); + WRITEFIXED(save->p, players[i].loop.revolution); + WRITEFIXED(save->p, players[i].loop.min_revolution); + WRITEFIXED(save->p, players[i].loop.max_revolution); + WRITEANGLE(save->p, players[i].loop.yaw); + WRITEFIXED(save->p, players[i].loop.origin.x); + WRITEFIXED(save->p, players[i].loop.origin.y); + WRITEFIXED(save->p, players[i].loop.origin.z); + WRITEFIXED(save->p, players[i].loop.shift.x); + WRITEFIXED(save->p, players[i].loop.shift.y); + WRITEUINT8(save->p, players[i].loop.flip); } } @@ -834,6 +847,19 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].itemRoulette.elapsed = (tic_t)READUINT32(save->p); players[i].itemRoulette.eggman = (boolean)READUINT8(save->p); + // sonicloopsvars_t + players[i].loop.radius = READFIXED(save->p); + players[i].loop.revolution = READFIXED(save->p); + players[i].loop.min_revolution = READFIXED(save->p); + players[i].loop.max_revolution = READFIXED(save->p); + players[i].loop.yaw = READANGLE(save->p); + players[i].loop.origin.x = READFIXED(save->p); + players[i].loop.origin.y = READFIXED(save->p); + players[i].loop.origin.z = READFIXED(save->p); + players[i].loop.shift.x = READFIXED(save->p); + players[i].loop.shift.y = READFIXED(save->p); + players[i].loop.flip = READUINT8(save->p); + //players[i].viewheight = P_GetPlayerViewHeight(players[i]); // scale cannot be factored in at this point } } diff --git a/src/p_setup.c b/src/p_setup.c index 0c8ddef6b..d97696309 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -334,7 +334,7 @@ FUNCNORETURN static ATTRNORETURN void CorruptMapError(const char *msg) { sprintf(mapname, "ID %d", gamemap-1); } - + CON_LogMessage("Map "); CON_LogMessage(mapname); CON_LogMessage(" is corrupt: "); @@ -380,7 +380,7 @@ void P_DeleteFlickies(INT16 i) static void P_ClearSingleMapHeaderInfo(INT16 num) { UINT8 i = 0; - + mapheaderinfo[num]->lvlttl[0] = '\0'; mapheaderinfo[num]->subttl[0] = '\0'; mapheaderinfo[num]->zonttl[0] = '\0'; @@ -677,11 +677,26 @@ void P_ReloadRings(void) } } +static int cmp_loopends(const void *a, const void *b) +{ + const mapthing_t + *mt1 = *(const mapthing_t*const*)a, + *mt2 = *(const mapthing_t*const*)b; + + // weighted sorting; tag takes precedence over type + return + intsign(Tag_FGet(&mt1->tags) - Tag_FGet(&mt2->tags)) * 2 + + intsign(mt1->args[0] - mt2->args[0]); +} + static void P_SpawnMapThings(boolean spawnemblems) { size_t i; mapthing_t *mt; + mapthing_t **loopends; + size_t num_loopends = 0; + // Spawn axis points first so they are at the front of the list for fast searching. for (i = 0, mt = mapthings; i < nummapthings; i++, mt++) { @@ -690,20 +705,32 @@ static void P_SpawnMapThings(boolean spawnemblems) case 1700: // MT_AXIS case 1701: // MT_AXISTRANSFER case 1702: // MT_AXISTRANSFERLINE + case 2021: // MT_LOOPCENTERPOINT mt->mobj = NULL; P_SpawnMapThing(mt); break; + case 2020: // MT_LOOPENDPOINT + num_loopends++; + break; default: break; } } + Z_Malloc(num_loopends * sizeof *loopends, PU_STATIC, + &loopends); + num_loopends = 0; + for (i = 0, mt = mapthings; i < nummapthings; i++, mt++) { - if (mt->type == 1700 // MT_AXIS - || mt->type == 1701 // MT_AXISTRANSFER - || mt->type == 1702) // MT_AXISTRANSFERLINE - continue; // These were already spawned + switch (mt->type) + { + case 1700: // MT_AXIS + case 1701: // MT_AXISTRANSFER + case 1702: // MT_AXISTRANSFERLINE + case 2021: // MT_LOOPCENTERPOINT + continue; // These were already spawned + } if (mt->type == mobjinfo[MT_BATTLECAPSULE].doomednum) continue; // This will spawn later @@ -713,6 +740,13 @@ static void P_SpawnMapThings(boolean spawnemblems) mt->mobj = NULL; + if (mt->type == mobjinfo[MT_LOOPENDPOINT].doomednum) + { + loopends[num_loopends] = mt; + num_loopends++; + continue; + } + if (mt->type >= 600 && mt->type <= 611) // item patterns P_SpawnItemPattern(mt); else if (mt->type == 1713) // hoops @@ -720,6 +754,25 @@ static void P_SpawnMapThings(boolean spawnemblems) else // Everything else P_SpawnMapThing(mt); } + + qsort(loopends, num_loopends, sizeof *loopends, + cmp_loopends); + + for (i = 1; i < num_loopends; ++i) + { + mapthing_t + *mt1 = loopends[i - 1], + *mt2 = loopends[i]; + + if (Tag_FGet(&mt1->tags) == Tag_FGet(&mt2->tags) && + mt1->args[0] == mt2->args[0]) + { + P_SpawnItemLine(mt1, mt2); + i++; + } + } + + Z_Free(loopends); } // Experimental groovy write function! @@ -3928,6 +3981,8 @@ static void P_AddBinaryMapTags(void) case 292: case 294: case 780: + case 2020: // MT_LOOPENDPOINT + case 2021: // MT_LOOPCENTERPOINT Tag_FSet(&mapthings[i].tags, mapthings[i].extrainfo); break; default: @@ -5952,6 +6007,7 @@ static void P_ConvertBinaryLinedefTypes(void) lines[i].args[1] |= TMBOT_FORCEDIR; lines[i].args[2] = sides[lines[i].sidenum[0]].textureoffset / FRACUNIT; + break; default: break; } @@ -6732,6 +6788,17 @@ static void P_ConvertBinaryThingTypes(void) mapthings[i].args[2] |= TMICF_INVERTSIZE; } break; + case 2020: // MT_LOOPENDPOINT + { + mapthings[i].args[0] = + mapthings[i].options & MTF_AMBUSH ? + TMLOOP_BETA : TMLOOP_ALPHA; + break; + } + case 2021: // MT_LOOPCENTERPOINT + mapthings[i].args[0] = (mapthings[i].options & MTF_AMBUSH) == MTF_AMBUSH; + mapthings[i].args[1] = mapthings[i].angle; + break; case 2050: // MT_DUELBOMB mapthings[i].args[1] = !!(mapthings[i].options & MTF_AMBUSH); break; @@ -7499,9 +7566,14 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) // This is needed. Don't touch. maptol = mapheaderinfo[gamemap-1]->typeoflevel; + // HWR2 skip 3d render draw hack to avoid losing the current wipe screen + g_wipeskiprender = true; + CON_Drawer(); // let the user know what we are going to do I_FinishUpdate(); // page flip or blit buffer + g_wipeskiprender = false; + // Reset the palette if (rendermode != render_none) V_SetPaletteLump("PLAYPAL"); @@ -7514,6 +7586,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) // Clear CECHO messages HU_ClearCEcho(); + HU_ClearTitlecardCEcho(); if (mapheaderinfo[gamemap-1]->runsoc[0] != '#') P_RunSOC(mapheaderinfo[gamemap-1]->runsoc); @@ -7550,7 +7623,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) F_WipeEndScreen(); S_StartSound(NULL, sfx_ruby1); - F_RunWipe(wipedefs[wipe_encore_toinvert], false, NULL, false, false); + F_RunWipe(wipe_encore_toinvert, wipedefs[wipe_encore_toinvert], false, NULL, false, false); // Hold on invert for extra effect. // (This define might be useful for other areas of code? Not sure) @@ -7565,8 +7638,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) I_UpdateTime(cv_timescale.value); \ } \ lastwipetic = nowtime; \ - if (moviemode) \ - M_SaveFrame(); \ + if (moviemode && rendermode == render_opengl) \ + M_LegacySaveFrame(); \ NetKeepAlive(); \ } \ @@ -7579,7 +7652,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 0); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_encore_towhite], false, "FADEMAP1", false, true); // wiggle the screen during this! + F_RunWipe(wipe_encore_towhite, wipedefs[wipe_encore_towhite], false, "FADEMAP1", false, true); // wiggle the screen during this! // THEN fade to a black screen. F_WipeStartScreen(); @@ -7587,7 +7660,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); F_WipeEndScreen(); - F_RunWipe(wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_level_toblack, wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); // Wait a bit longer. WAIT((3*TICRATE)/4); @@ -7595,8 +7668,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) else { // dedicated servers can call this now, to wait the appropriate amount of time for clients to wipe - F_RunWipe(wipedefs[wipe_encore_towhite], false, "FADEMAP1", false, true); - F_RunWipe(wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); + F_RunWipe(wipe_encore_towhite, wipedefs[wipe_encore_towhite], false, "FADEMAP1", false, true); + F_RunWipe(wipe_level_toblack, wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false); } } @@ -7620,6 +7693,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) // But only if we didn't do the encore startup wipe if (!demo.rewinding && !reloadinggamestate) { + int wipetype = wipe_level_toblack; // Fade out music here. Deduct 2 tics so the fade volume actually reaches 0. // But don't halt the music! S_Start will take care of that. This dodges a MIDI crash bug. @@ -7649,10 +7723,12 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) if (ranspecialwipe != 2) S_StartSound(NULL, sfx_s3kaf); levelfadecol = 0; + wipetype = wipe_encore_towhite; } else if (encoremode) { levelfadecol = 0; + wipetype = wipe_encore_towhite; } else { @@ -7667,7 +7743,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) F_WipeEndScreen(); } - F_RunWipe(wipedefs[wipe_level_toblack], false, ((levelfadecol == 0) ? "FADEMAP1" : "FADEMAP0"), false, false); + F_RunWipe(wipetype, wipedefs[wipetype], false, ((levelfadecol == 0) ? "FADEMAP1" : "FADEMAP0"), false, false); } /*if (!titlemapinaction) wipegamestate = GS_LEVEL;*/ diff --git a/src/p_spec.h b/src/p_spec.h index 6ccb328cd..908046936 100644 --- a/src/p_spec.h +++ b/src/p_spec.h @@ -513,6 +513,12 @@ typedef enum TMBOT_FORCEDIR = 1<<2, } textmapbotcontroller_t; +typedef enum +{ + TMLOOP_ALPHA = 0, + TMLOOP_BETA = 1, +} textmaploopendpointtype_t; + // GETSECSPECIAL (specialval, section) // // Pulls out the special # from a particular section. diff --git a/src/p_tick.c b/src/p_tick.c index 91be6d183..70c4a14f3 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -647,24 +647,39 @@ void P_Ticker(boolean run) if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo)) P_PlayerAfterThink(&players[i]); - // Bosses have a punchy start, so no position. if (K_CheckBossIntro() == true) { + // Bosses have a punchy start, so no position. if (leveltime == 3) { S_ChangeMusic(mapmusname, mapmusflags, true); S_ShowMusicCredit(); } } - // Plays the music after the starting countdown. - else + else if (leveltime < starttime + TICRATE) { - if (leveltime == (starttime + (TICRATE/2))) + // Start countdown/music handling + if (leveltime == starttime-(3*TICRATE)) { + S_StartSound(NULL, sfx_s3ka7); // 3, + S_FadeMusic(0, 3500); //S_FadeOutStopMusic(3500); -- TODO the S_StopMusic callback can halt successor music instead + } + else if ((leveltime == starttime-(2*TICRATE)) || (leveltime == starttime-TICRATE)) + { + S_StartSound(NULL, sfx_s3ka7); // 2, 1, + } + else if (leveltime == starttime) + { + S_StartSound(NULL, sfx_s3kad); // GO! + } + else if (leveltime == (starttime + (TICRATE/2))) + { + // Plays the music after the starting countdown. S_ChangeMusic(mapmusname, mapmusflags, true); S_ShowMusicCredit(); } + // POSITION!! music if (encoremode) { // Encore humming starts immediately. diff --git a/src/p_user.c b/src/p_user.c index 223af8f47..b3c605159 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -2691,8 +2691,6 @@ static void P_DeathThink(player_t *player) else player->karthud[khud_timeovercam] = 0; - K_KartPlayerHUDUpdate(player); - if (player->pflags & PF_NOCONTEST) { playerGone = true; @@ -4227,6 +4225,11 @@ void P_PlayerThink(player_t *player) P_DoZoomTube(player); player->rmomx = player->rmomy = 0; } + else if (player->loop.radius != 0) + { + P_PlayerOrbit(player); + player->rmomx = player->rmomy = 0; + } else { // Move around. diff --git a/src/r_main.c b/src/r_main.c index 2c781509c..b00717810 100644 --- a/src/r_main.c +++ b/src/r_main.c @@ -1476,9 +1476,15 @@ void R_RenderPlayerView(void) if (cv_homremoval.value && player == &players[displayplayers[0]]) { if (cv_homremoval.value == 1) - V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); // No HOM effect! - else //'development' HOM removal -- makes it blindingly obvious if HOM is spotted. - V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 32+(timeinmap&15)); + { + // Clear the software screen buffer to remove HOM + memset(screens[0], 31, vid.width * vid.height * vid.bpp); + } + else + { + //'development' HOM removal -- makes it blindingly obvious if HOM is spotted. + memset(screens[0], 32+(timeinmap&15), vid.width * vid.height * vid.bpp); + } } else if (r_splitscreen == 2 && player == &players[displayplayers[2]]) { diff --git a/src/r_patch.cpp b/src/r_patch.cpp index a27e8035c..443e940a3 100644 --- a/src/r_patch.cpp +++ b/src/r_patch.cpp @@ -103,6 +103,7 @@ void Patch_Free(patch_t *patch) { if (!patch || patch == missingpat) return; + Patch_FreeData(patch); Z_Free(patch); } diff --git a/src/rhi/gl3_core/gl3_core_rhi.cpp b/src/rhi/gl3_core/gl3_core_rhi.cpp index 7b9500594..856913d53 100644 --- a/src/rhi/gl3_core/gl3_core_rhi.cpp +++ b/src/rhi/gl3_core/gl3_core_rhi.cpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "gl3_core_rhi.hpp" #include @@ -13,7 +22,7 @@ using namespace srb2; using namespace rhi; -#if 1 +#ifndef NDEBUG #define GL_ASSERT \ { \ GLenum __err = gl_->GetError(); \ @@ -33,6 +42,12 @@ constexpr GLenum map_pixel_format(rhi::PixelFormat format) { switch (format) { + case rhi::PixelFormat::kR8: + return GL_R8; + case rhi::PixelFormat::kRG8: + return GL_RG8; + case rhi::PixelFormat::kRGB8: + return GL_RGB8; case rhi::PixelFormat::kRGBA8: return GL_RGBA8; case rhi::PixelFormat::kDepth16: @@ -56,6 +71,16 @@ constexpr std::tuple map_pixel_data_format(rhi::PixelFor type = GL_UNSIGNED_BYTE; size = 1; break; + case rhi::PixelFormat::kRG8: + layout = GL_RG; + type = GL_UNSIGNED_BYTE; + size = 2; + break; + case rhi::PixelFormat::kRGB8: + layout = GL_RGB; + type = GL_UNSIGNED_BYTE; + size = 3; + break; case rhi::PixelFormat::kRGBA8: layout = GL_RGBA; type = GL_UNSIGNED_BYTE; @@ -77,6 +102,27 @@ constexpr GLenum map_texture_format(rhi::TextureFormat format) return GL_RGB; case rhi::TextureFormat::kLuminance: return GL_RED; + case rhi::TextureFormat::kLuminanceAlpha: + return GL_RG; + default: + return GL_ZERO; + } +} + +constexpr GLenum map_internal_texture_format(rhi::TextureFormat format) +{ + switch (format) + { + case rhi::TextureFormat::kRGBA: + return GL_RGBA8; + case rhi::TextureFormat::kRGB: + return GL_RGB8; + case rhi::TextureFormat::kLuminance: + return GL_R8; + case rhi::TextureFormat::kLuminanceAlpha: + return GL_RG8; + case rhi::TextureFormat::kDepth: + return GL_DEPTH_COMPONENT24; default: return GL_ZERO; } @@ -286,6 +332,35 @@ constexpr const char* map_uniform_attribute_symbol_name(rhi::UniformName name) return "u_projection"; case rhi::UniformName::kTexCoord0Transform: return "u_texcoord0_transform"; + case rhi::UniformName::kSampler0IsIndexedAlpha: + return "u_sampler0_is_indexed_alpha"; + case rhi::UniformName::kWipeColorizeMode: + return "u_wipe_colorize_mode"; + case rhi::UniformName::kWipeEncoreSwizzle: + return "u_wipe_encore_swizzle"; + default: + return nullptr; + } +} + +constexpr const char* map_uniform_enable_define(rhi::UniformName name) +{ + switch (name) + { + case rhi::UniformName::kTime: + return "ENABLE_U_TIME"; + case rhi::UniformName::kProjection: + return "ENABLE_U_PROJECTION"; + case rhi::UniformName::kModelView: + return "ENABLE_U_MODELVIEW"; + case rhi::UniformName::kTexCoord0Transform: + return "ENABLE_U_TEXCOORD0_TRANSFORM"; + case rhi::UniformName::kSampler0IsIndexedAlpha: + return "ENABLE_U_SAMPLER0_IS_INDEXED_ALPHA"; + case rhi::UniformName::kWipeColorizeMode: + return "ENABLE_U_WIPE_COLORIZE_MODE"; + case rhi::UniformName::kWipeEncoreSwizzle: + return "ENABLE_U_WIPE_ENCORE_SWIZZLE"; default: return nullptr; } @@ -308,6 +383,23 @@ constexpr const char* map_sampler_symbol_name(rhi::SamplerName name) } } +constexpr const char* map_sampler_enable_define(rhi::SamplerName name) +{ + switch (name) + { + case rhi::SamplerName::kSampler0: + return "ENABLE_S_SAMPLER0"; + case rhi::SamplerName::kSampler1: + return "ENABLE_S_SAMPLER1"; + case rhi::SamplerName::kSampler2: + return "ENABLE_S_SAMPLER2"; + case rhi::SamplerName::kSampler3: + return "ENABLE_S_SAMPLER3"; + default: + return nullptr; + } +} + constexpr GLenum map_vertex_attribute_format(rhi::VertexAttributeFormat format) { switch (format) @@ -423,8 +515,13 @@ rhi::Handle GlCoreRhi::create_texture(const rhi::TextureDesc& desc { SRB2_ASSERT(graphics_context_active_ == false); - GLenum internal_format = map_texture_format(desc.format); + GLenum internal_format = map_internal_texture_format(desc.format); SRB2_ASSERT(internal_format != GL_ZERO); + GLenum format = GL_RGBA; + if (desc.format == TextureFormat::kDepth) + { + format = GL_DEPTH_COMPONENT; + } GLuint name = 0; gl_->GenTextures(1, &name); @@ -439,7 +536,7 @@ rhi::Handle GlCoreRhi::create_texture(const rhi::TextureDesc& desc GL_ASSERT gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); GL_ASSERT - gl_->TexImage2D(GL_TEXTURE_2D, 0, internal_format, desc.width, desc.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + gl_->TexImage2D(GL_TEXTURE_2D, 0, internal_format, desc.width, desc.height, 0, format, GL_UNSIGNED_BYTE, nullptr); GL_ASSERT GlCoreTexture texture; @@ -478,13 +575,19 @@ void GlCoreRhi::update_texture( SRB2_ASSERT(texture_slab_.is_valid(texture) == true); auto& t = texture_slab_[texture]; + // Each row of pixels must be on the unpack alignment boundary. + // This alignment is not user changeable until OpenGL 4. + constexpr const int32_t kUnpackAlignment = 4; + GLenum format = GL_RGBA; GLenum type = GL_UNSIGNED_BYTE; GLuint size = 0; std::tie(format, type, size) = map_pixel_data_format(data_format); SRB2_ASSERT(format != GL_ZERO && type != GL_ZERO); SRB2_ASSERT(map_texture_format(t.desc.format) == format); - SRB2_ASSERT(region.w * region.h * size == data.size_bytes()); + + int32_t expected_row_span = (((size * region.w) + kUnpackAlignment - 1) / kUnpackAlignment) * kUnpackAlignment; + SRB2_ASSERT(expected_row_span * region.h == data.size_bytes()); SRB2_ASSERT(region.x + region.w <= t.desc.width && region.y + region.h <= t.desc.height); gl_->ActiveTexture(GL_TEXTURE0); @@ -740,14 +843,79 @@ rhi::Handle GlCoreRhi::create_pipeline(const PipelineDesc& desc) } } } + for (auto& uniform_group : desc.uniform_input.enabled_uniforms) + { + for (auto& uniform : uniform_group) + { + for (auto const& req_uni_group : reqs.uniforms.uniform_groups) + { + for (auto const& req_uni : req_uni_group) + { + if (req_uni.name == uniform && !req_uni.required) + { + vert_src_processed.append("#define "); + vert_src_processed.append(map_uniform_enable_define(uniform)); + vert_src_processed.append("\n"); + } + } + } + } + } + } + string_i = new_i + 1; + } while (string_i != std::string::npos); + + std::string frag_src_processed; + string_i = 0; + do + { + std::string::size_type new_i = frag_src.find('\n', string_i); + if (new_i == std::string::npos) + { + break; + } + std::string_view line_view(frag_src.c_str() + string_i, new_i - string_i + 1); + frag_src_processed.append(line_view); + if (line_view.rfind("#version ", 0) == 0) + { + for (auto& sampler : desc.sampler_input.enabled_samplers) + { + for (auto const& require_sampler : reqs.samplers.samplers) + { + if (sampler == require_sampler.name && !require_sampler.required) + { + frag_src_processed.append("#define "); + frag_src_processed.append(map_sampler_enable_define(sampler)); + frag_src_processed.append("\n"); + } + } + } + for (auto& uniform_group : desc.uniform_input.enabled_uniforms) + { + for (auto& uniform : uniform_group) + { + for (auto const& req_uni_group : reqs.uniforms.uniform_groups) + { + for (auto const& req_uni : req_uni_group) + { + if (req_uni.name == uniform && !req_uni.required) + { + frag_src_processed.append("#define "); + frag_src_processed.append(map_uniform_enable_define(uniform)); + frag_src_processed.append("\n"); + } + } + } + } + } } string_i = new_i + 1; } while (string_i != std::string::npos); const char* vert_src_arr[1] = {vert_src_processed.c_str()}; const GLint vert_src_arr_lens[1] = {static_cast(vert_src_processed.size())}; - const char* frag_src_arr[1] = {frag_src.c_str()}; - const GLint frag_src_arr_lens[1] = {static_cast(frag_src.size())}; + const char* frag_src_arr[1] = {frag_src_processed.c_str()}; + const GLint frag_src_arr_lens[1] = {static_cast(frag_src_processed.size())}; vertex = gl_->CreateShader(GL_VERTEX_SHADER); gl_->ShaderSource(vertex, 1, vert_src_arr, vert_src_arr_lens); @@ -1380,6 +1548,8 @@ void GlCoreRhi::bind_index_buffer(Handle ctx, Handle bu SRB2_ASSERT(ib.desc.type == rhi::BufferType::kIndexBuffer); + current_index_buffer_ = buffer; + gl_->BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ib.buffer); } @@ -1412,22 +1582,37 @@ void GlCoreRhi::draw(Handle ctx, uint32_t vertex_count, uint32_ void GlCoreRhi::draw_indexed(Handle ctx, uint32_t index_count, uint32_t first_index) { SRB2_ASSERT(graphics_context_active_ == true && graphics_context_generation_ == ctx.generation()); + + SRB2_ASSERT(current_index_buffer_ != kNullHandle); +#ifndef NDEBUG + { + auto& ib = buffer_slab_[current_index_buffer_]; + SRB2_ASSERT((index_count + first_index) * 2 + index_buffer_offset_ <= ib.desc.size); + } +#endif + gl_->DrawElements( map_primitive_mode(current_primitive_type_), index_count, GL_UNSIGNED_SHORT, - reinterpret_cast(first_index * 2 + index_buffer_offset_) + (const void*)((size_t)first_index * 2 + index_buffer_offset_) ); GL_ASSERT } -void GlCoreRhi::read_pixels(Handle ctx, const Rect& rect, tcb::span out) +void GlCoreRhi::read_pixels(Handle ctx, const Rect& rect, PixelFormat format, tcb::span out) { SRB2_ASSERT(graphics_context_active_ == true && graphics_context_generation_ == ctx.generation()); SRB2_ASSERT(current_render_pass_.has_value()); - SRB2_ASSERT(out.size_bytes() == rect.w * rect.h); - gl_->ReadPixels(rect.x, rect.y, rect.w, rect.h, GL_RGBA, GL_UNSIGNED_BYTE, out.data()); + std::tuple gl_format = map_pixel_data_format(format); + GLenum layout = std::get<0>(gl_format); + GLenum type = std::get<1>(gl_format); + GLint size = std::get<2>(gl_format); + + SRB2_ASSERT(out.size_bytes() == rect.w * rect.h * size); + + gl_->ReadPixels(rect.x, rect.y, rect.w, rect.h, layout, type, out.data()); } void GlCoreRhi::finish() diff --git a/src/rhi/gl3_core/gl3_core_rhi.hpp b/src/rhi/gl3_core/gl3_core_rhi.hpp index fa7997b8b..548045463 100644 --- a/src/rhi/gl3_core/gl3_core_rhi.hpp +++ b/src/rhi/gl3_core/gl3_core_rhi.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_RHI_GLES2_RHI_HPP__ #define __SRB2_RHI_GLES2_RHI_HPP__ @@ -145,6 +154,8 @@ class GlCoreRhi final : public Rhi Slab uniform_set_slab_; Slab binding_set_slab_; + Handle current_index_buffer_; + std::unordered_map framebuffers_ {16}; struct DefaultRenderPassState @@ -215,7 +226,8 @@ public: virtual void set_viewport(Handle ctx, const Rect& rect) override; virtual void draw(Handle ctx, uint32_t vertex_count, uint32_t first_vertex) override; virtual void draw_indexed(Handle ctx, uint32_t index_count, uint32_t first_index) override; - virtual void read_pixels(Handle ctx, const Rect& rect, tcb::span out) override; + virtual void + read_pixels(Handle ctx, const Rect& rect, PixelFormat format, tcb::span out) override; virtual void present() override; diff --git a/src/rhi/gles2/gles2_rhi.cpp b/src/rhi/gles2/gles2_rhi.cpp index 5c8134eb6..a1eb92acf 100644 --- a/src/rhi/gles2/gles2_rhi.cpp +++ b/src/rhi/gles2/gles2_rhi.cpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "gles2_rhi.hpp" #include diff --git a/src/rhi/gles2/gles2_rhi.hpp b/src/rhi/gles2/gles2_rhi.hpp index 9858e770b..f912941b4 100644 --- a/src/rhi/gles2/gles2_rhi.hpp +++ b/src/rhi/gles2/gles2_rhi.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_RHI_GLES2_RHI_HPP__ #define __SRB2_RHI_GLES2_RHI_HPP__ diff --git a/src/rhi/handle.hpp b/src/rhi/handle.hpp index bda2928fa..282a924da 100644 --- a/src/rhi/handle.hpp +++ b/src/rhi/handle.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_RHI_HANDLE_HPP__ #define __SRB2_RHI_HANDLE_HPP__ diff --git a/src/rhi/rhi.cpp b/src/rhi/rhi.cpp index c63282b95..36c3c7c8e 100644 --- a/src/rhi/rhi.cpp +++ b/src/rhi/rhi.cpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "rhi.hpp" #include @@ -14,8 +23,9 @@ const ProgramRequirements srb2::rhi::kProgramRequirementsUnshaded = { ProgramVertexInput {VertexAttributeName::kTexCoord0, VertexAttributeFormat::kFloat2, false}, ProgramVertexInput {VertexAttributeName::kColor, VertexAttributeFormat::kFloat4, false}}}, ProgramUniformRequirements { - {{{UniformName::kProjection}}, {{UniformName::kModelView, UniformName::kTexCoord0Transform}}}}, - ProgramSamplerRequirements {{ProgramSamplerInput {SamplerName::kSampler0, true}}}}; + {{{{UniformName::kProjection, true}}}, + {{{UniformName::kModelView, true}, {UniformName::kTexCoord0Transform, true}}}}}, + ProgramSamplerRequirements {{{SamplerName::kSampler0, true}}}}; const ProgramRequirements srb2::rhi::kProgramRequirementsUnshadedPaletted = { ProgramVertexInputRequirements { @@ -23,9 +33,22 @@ const ProgramRequirements srb2::rhi::kProgramRequirementsUnshadedPaletted = { ProgramVertexInput {VertexAttributeName::kTexCoord0, VertexAttributeFormat::kFloat2, false}, ProgramVertexInput {VertexAttributeName::kColor, VertexAttributeFormat::kFloat4, false}}}, ProgramUniformRequirements { - {{{UniformName::kProjection}}, {{UniformName::kModelView, UniformName::kTexCoord0Transform}}}}, + {{{{UniformName::kProjection, true}}}, + {{{UniformName::kModelView, true}, + {UniformName::kTexCoord0Transform, true}, + {UniformName::kSampler0IsIndexedAlpha, false}}}}}, ProgramSamplerRequirements { - {ProgramSamplerInput {SamplerName::kSampler0, true}, ProgramSamplerInput {SamplerName::kSampler1, true}}}}; + {{SamplerName::kSampler0, true}, {SamplerName::kSampler1, true}, {SamplerName::kSampler2, false}}}}; + +const ProgramRequirements srb2::rhi::kProgramRequirementsPostprocessWipe = { + ProgramVertexInputRequirements { + {ProgramVertexInput {VertexAttributeName::kPosition, VertexAttributeFormat::kFloat3, true}, + ProgramVertexInput {VertexAttributeName::kTexCoord0, VertexAttributeFormat::kFloat2, true}}}, + ProgramUniformRequirements { + {{{{UniformName::kProjection, true}, + {UniformName::kWipeColorizeMode, true}, + {UniformName::kWipeEncoreSwizzle, true}}}}}, + ProgramSamplerRequirements {{{SamplerName::kSampler0, true}, {SamplerName::kSampler1, true}, {SamplerName::kSampler2, true}}}}; const ProgramRequirements& rhi::program_requirements_for_program(PipelineProgram program) noexcept { @@ -35,6 +58,8 @@ const ProgramRequirements& rhi::program_requirements_for_program(PipelineProgram return kProgramRequirementsUnshaded; case PipelineProgram::kUnshadedPaletted: return kProgramRequirementsUnshadedPaletted; + case PipelineProgram::kPostprocessWipe: + return kProgramRequirementsPostprocessWipe; default: std::terminate(); } diff --git a/src/rhi/rhi.hpp b/src/rhi/rhi.hpp index a44423aee..a6186ba97 100644 --- a/src/rhi/rhi.hpp +++ b/src/rhi/rhi.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_RHI_RHI_HPP__ #define __SRB2_RHI_RHI_HPP__ @@ -63,6 +72,8 @@ enum class UniformFormat enum class PixelFormat { kR8, + kRG8, + kRGB8, kRGBA8, kDepth16, kStencil8 @@ -71,8 +82,10 @@ enum class PixelFormat enum class TextureFormat { kLuminance, + kLuminanceAlpha, kRGB, - kRGBA + kRGBA, + kDepth }; enum class CompareFunc @@ -152,7 +165,8 @@ enum class AttachmentStoreOp enum class PipelineProgram { kUnshaded, - kUnshadedPaletted + kUnshadedPaletted, + kPostprocessWipe }; enum class BufferType @@ -181,7 +195,10 @@ enum class UniformName kTime, kModelView, kProjection, - kTexCoord0Transform + kTexCoord0Transform, + kSampler0IsIndexedAlpha, + kWipeColorizeMode, + kWipeEncoreSwizzle }; enum class SamplerName @@ -237,12 +254,12 @@ struct ProgramVertexInputRequirements struct ProgramUniformRequirements { - srb2::StaticVec, 4> uniform_groups; + srb2::StaticVec, 4> uniform_groups; }; struct ProgramSamplerRequirements { - std::array, kMaxSamplers> samplers; + srb2::StaticVec samplers; }; struct ProgramRequirements @@ -254,6 +271,7 @@ struct ProgramRequirements extern const ProgramRequirements kProgramRequirementsUnshaded; extern const ProgramRequirements kProgramRequirementsUnshadedPaletted; +extern const ProgramRequirements kProgramRequirementsPostprocessWipe; const ProgramRequirements& program_requirements_for_program(PipelineProgram program) noexcept; @@ -288,6 +306,12 @@ inline constexpr const UniformFormat uniform_format(UniformName name) noexcept return UniformFormat::kMat4; case UniformName::kTexCoord0Transform: return UniformFormat::kMat3; + case UniformName::kSampler0IsIndexedAlpha: + return UniformFormat::kInt; + case UniformName::kWipeColorizeMode: + return UniformFormat::kInt; + case UniformName::kWipeEncoreSwizzle: + return UniformFormat::kInt; default: return UniformFormat::kFloat; } @@ -309,8 +333,8 @@ struct VertexAttributeLayoutDesc struct VertexInputDesc { - std::vector buffer_layouts; - std::vector attr_layouts; + srb2::StaticVec buffer_layouts; + srb2::StaticVec attr_layouts; }; struct UniformInputDesc @@ -489,6 +513,9 @@ struct GraphicsContext { }; +/// @brief The unpack alignment of a row span when uploading pixels to the device. +constexpr const std::size_t kPixelRowUnpackAlignment = 4; + /// @brief An active handle to a rendering device. struct Rhi { @@ -542,7 +569,8 @@ struct Rhi virtual void set_viewport(Handle ctx, const Rect& rect) = 0; virtual void draw(Handle ctx, uint32_t vertex_count, uint32_t first_vertex) = 0; virtual void draw_indexed(Handle ctx, uint32_t index_count, uint32_t first_index) = 0; - virtual void read_pixels(Handle ctx, const Rect& rect, tcb::span out) = 0; + virtual void + read_pixels(Handle ctx, const Rect& rect, PixelFormat format, tcb::span out) = 0; virtual void present() = 0; diff --git a/src/sdl/i_video.cpp b/src/sdl/i_video.cpp index bf5deb873..d86f4a4b4 100644 --- a/src/sdl/i_video.cpp +++ b/src/sdl/i_video.cpp @@ -231,7 +231,11 @@ static void SDLSetMode(INT32 width, INT32 height, SDL_bool fullscreen, SDL_bool { OglSdlSurface(vid.width, vid.height); } + else #endif + { + SDL_GL_SetSwapInterval(cv_vidwait.value ? 1 : 0); + } SDL_GetWindowSize(window, &width, &height); vid.realwidth = static_cast(width); diff --git a/src/sdl/new_sound.cpp b/src/sdl/new_sound.cpp index 228985c83..80f549508 100644 --- a/src/sdl/new_sound.cpp +++ b/src/sdl/new_sound.cpp @@ -21,7 +21,10 @@ #include "../audio/sound_effect_player.hpp" #include "../cxxutil.hpp" #include "../io/streams.hpp" + +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #include "../m_avrecorder.hpp" +#endif #include "../doomdef.h" #include "../i_sound.h" @@ -58,7 +61,9 @@ static shared_ptr> gain_music; static vector> sound_effect_channels; +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES static shared_ptr av_recorder; +#endif static void (*music_fade_callback)(); @@ -138,9 +143,10 @@ void audio_callback(void* userdata, Uint8* buffer, int len) std::clamp(float_buffer[i].amplitudes[1], -1.f, 1.f), }; } - +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES if (av_recorder) av_recorder->push_audio_samples(tcb::span {float_buffer, float_len}); +#endif } catch (...) { @@ -758,8 +764,10 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping) void I_UpdateAudioRecorder(void) { +#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES // must be locked since av_recorder is used by audio_callback SdlAudioLockHandle _; av_recorder = g_av_recorder; +#endif } diff --git a/src/sdl/rhi_gl3_core_platform.cpp b/src/sdl/rhi_gl3_core_platform.cpp index d8c0cde33..5de7eef70 100644 --- a/src/sdl/rhi_gl3_core_platform.cpp +++ b/src/sdl/rhi_gl3_core_platform.cpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "rhi_gl3_core_platform.hpp" #include @@ -33,6 +42,10 @@ std::tuple SdlGlCorePlatform::find_shader_sources(rhi: vertex_lump_name = "rhi_glcore_vertex_unshadedpaletted"; fragment_lump_name = "rhi_glcore_fragment_unshadedpaletted"; break; + case rhi::PipelineProgram::kPostprocessWipe: + vertex_lump_name = "rhi_glcore_vertex_postprocesswipe"; + fragment_lump_name = "rhi_glcore_fragment_postprocesswipe"; + break; default: std::terminate(); } diff --git a/src/sdl/rhi_gl3_core_platform.hpp b/src/sdl/rhi_gl3_core_platform.hpp index 0c0f6f4f3..9522e4ba4 100644 --- a/src/sdl/rhi_gl3_core_platform.hpp +++ b/src/sdl/rhi_gl3_core_platform.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_SDL_RHI_GLES2_PLATFORM_HPP__ #define __SRB2_SDL_RHI_GLES2_PLATFORM_HPP__ diff --git a/src/sdl/rhi_gles2_platform.cpp b/src/sdl/rhi_gles2_platform.cpp index d91a3d2bf..edf5fe201 100644 --- a/src/sdl/rhi_gles2_platform.cpp +++ b/src/sdl/rhi_gles2_platform.cpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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 "rhi_gles2_platform.hpp" #include diff --git a/src/sdl/rhi_gles2_platform.hpp b/src/sdl/rhi_gles2_platform.hpp index 19970d8f1..b434c9c2e 100644 --- a/src/sdl/rhi_gles2_platform.hpp +++ b/src/sdl/rhi_gles2_platform.hpp @@ -1,3 +1,12 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// 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_SDL_RHI_GLES2_PLATFORM_HPP__ #define __SRB2_SDL_RHI_GLES2_PLATFORM_HPP__ diff --git a/src/st_stuff.c b/src/st_stuff.c index 01350c6ce..289a6c3a7 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -1095,6 +1095,8 @@ void ST_preLevelTitleCardDrawer(void) // static void ST_overlayDrawer(void) { + const UINT8 viewnum = R_GetViewNumber(); + // hu_showscores = auto hide score/time/rings when tab rankings are shown if (!(hu_showscores && (netgame || multiplayer))) { @@ -1135,7 +1137,7 @@ static void ST_overlayDrawer(void) if (!hu_showscores && netgame && !mapreset) { - if (stplyr->spectator && LUA_HudEnabled(hud_textspectator)) + if (stplyr->spectator && displayplayers[viewnum] == g_localplayers[viewnum] && LUA_HudEnabled(hud_textspectator)) { const char *itemtxt = M_GetText("Item - Join Game"); diff --git a/src/tables.c b/src/tables.c index 8c4dc50e1..6a7b7645b 100644 --- a/src/tables.c +++ b/src/tables.c @@ -167,7 +167,7 @@ angle_t FixedAngle(fixed_t fa) return AngleAdj(cfa, cwf, ra); } -INT32 AngleDelta(angle_t a1, angle_t a2) +angle_t AngleDelta(angle_t a1, angle_t a2) { angle_t delta = a1 - a2; diff --git a/src/tables.h b/src/tables.h index ac3bfe8a5..1a1da82d3 100644 --- a/src/tables.h +++ b/src/tables.h @@ -27,7 +27,7 @@ extern "C" { #define FINEANGLES 8192 #define FINEMASK (FINEANGLES - 1) #define ANGLETOFINESHIFT 19 // 0x100000000 to 0x2000 -#define FINEANGLE_C(x) ((FixedAngle((x)*FRACUNIT)>>ANGLETOFINESHIFT) & FINEMASK) // ((x*(ANGLE_45/45))>>ANGLETOFINESHIFT) & FINEMASK +#define ANGLETOFINE(x) (((x)>>ANGLETOFINESHIFT) & FINEMASK) // Effective size is 10240. extern fixed_t finesine[5*FINEANGLES/4]; @@ -110,7 +110,7 @@ FUNCMATH angle_t FixedAngle(fixed_t fa); // and with a factor, with +factor for (fa/factor) and -factor for (fa*factor) FUNCMATH angle_t FixedAngleC(fixed_t fa, fixed_t factor); // difference between two angle_t -FUNCMATH INT32 AngleDelta(angle_t a1, angle_t a2); +FUNCMATH angle_t AngleDelta(angle_t a1, angle_t a2); FUNCMATH INT32 AngleDeltaSigned(angle_t a1, angle_t a2); FUNCMATH float AngleToFloat(angle_t x); FUNCMATH angle_t FloatToAngle(float f); @@ -136,6 +136,10 @@ void FM_Rotate(matrix_t *dest, angle_t angle, fixed_t x, fixed_t y, fixed_t z); #define FINECOSINE(n) (finecosine[n]>>(FINE_FRACBITS-FRACBITS)) #define FINETANGENT(n) (finetangent[n]>>(FINE_FRACBITS-FRACBITS)) +// FSIN(ANGLE_90) = FRACUNIT +#define FSIN(n) FINESINE(ANGLETOFINE(n)) +#define FCOS(n) FINECOSINE(ANGLETOFINE(n)) + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/v_video.cpp b/src/v_video.cpp index 779a3a6c6..65bc2b2ef 100644 --- a/src/v_video.cpp +++ b/src/v_video.cpp @@ -41,6 +41,8 @@ #include "k_boss.h" #include "i_time.h" +using namespace srb2; + // Each screen is [vid.width*vid.height]; UINT8 *screens[5]; // screens[0] = main display window @@ -97,8 +99,12 @@ RGBA_t *pLocalPalette = NULL; RGBA_t *pMasterPalette = NULL; RGBA_t *pGammaCorrectedPalette = NULL; +hwr2::Twodee srb2::g_2d; + static size_t currentPaletteSize; +static UINT8 softwaretranstohwr[11] = { 0, 25, 51, 76,102,127,153,178,204,229,255}; + /* The following was an extremely helpful resource when developing my Colour Cube LUT. http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter24.html @@ -650,7 +656,7 @@ void V_AdjustXYWithSnap(INT32 *x, INT32 *y, UINT32 options, INT32 dupx, INT32 du } } -static cliprect_t cliprect; +static cliprect_t cliprect = {0}; const cliprect_t *V_GetClipRect(void) { @@ -771,16 +777,11 @@ static inline UINT8 transmappedpdraw(const UINT8 *dest, const UINT8 *source, fix // Draws a patch scaled to arbitrary size. void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, INT32 scrn, patch_t *patch, const UINT8 *colormap) { - UINT8 (*patchdrawfunc)(const UINT8*, const UINT8*, fixed_t); UINT32 alphalevel, blendmode; - fixed_t col, ofs, colfrac, rowfrac, fdup, vdup; + fixed_t vdup; INT32 dupx, dupy; - const column_t *column; - UINT8 *desttop, *dest, *deststart, *destend; - const UINT8 *source, *deststop; fixed_t pwidth; // patch width - fixed_t offx = 0; // x offset const cliprect_t *clip = V_GetClipRect(); @@ -796,8 +797,6 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca } #endif - patchdrawfunc = standardpdraw; - if ((blendmode = ((scrn & V_BLENDMASK) >> V_BLENDSHIFT))) blendmode++; // realign to constants if ((alphalevel = ((scrn & V_ALPHAMASK) >> V_ALPHASHIFT))) @@ -812,15 +811,6 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca if (alphalevel >= 10) // Still inelegible to render? return; } - if ((v_translevel = R_GetBlendTable(blendmode, alphalevel))) - patchdrawfunc = translucentpdraw; - - v_colormap = NULL; - if (colormap) - { - v_colormap = colormap; - patchdrawfunc = (v_translevel) ? transmappedpdraw : mappedpdraw; - } dupx = vid.dupx; dupy = vid.dupy; @@ -843,11 +833,9 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca // only use one dup, to avoid stretching (har har) dupx = dupy = (dupx < dupy ? dupx : dupy); - fdup = vdup = FixedMul(dupx<>= FRACBITS; y >>= FRACBITS; - desttop += (y*vid.width) + x; } else { @@ -894,8 +874,6 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca { V_AdjustXYWithSnap(&x, &y, scrn, dupx, dupy); } - - desttop += (y*vid.width) + x; } if (pscale != FRACUNIT) // scale width properly @@ -908,104 +886,73 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca else pwidth = patch->width * dupx; - deststart = desttop; - destend = desttop + pwidth; + float fdupy = FIXED_TO_FLOAT(vdup); - for (col = 0; (col>>FRACBITS) < patch->width; col += colfrac, ++offx, desttop++) + float fx = x; + float fy = y; + float fx2 = fx + pwidth; + float fy2 = fy + static_cast(patch->height) * fdupy; + float falpha = 1.f; + float umin = 0.f; + float umax = 1.f; + float vmin = 0.f; + float vmax = 1.f; + + // flip UVs + if (scrn & V_FLIP) { - INT32 topdelta, prevdelta = -1; - - if (scrn & V_FLIP) // offx is measured from right edge instead of left - { - if (x+pwidth-offx < (clip ? clip->left : 0)) // don't draw off the left of the screen (WRAP PREVENTION) - break; - if (x+pwidth-offx >= (clip ? clip->right : vid.width)) // don't draw off the right of the screen (WRAP PREVENTION) - continue; - } - else - { - if (x+offx < (clip ? clip->left : 0)) // don't draw off the left of the screen (WRAP PREVENTION) - continue; - if (x+offx >= (clip ? clip->right : vid.width)) // don't draw off the right of the screen (WRAP PREVENTION) - break; - } - - column = (const column_t *)((const UINT8 *)(patch->columns) + (patch->columnofs[col>>FRACBITS])); - - while (column->topdelta != 0xff) - { - fixed_t offy = 0; - - topdelta = column->topdelta; - if (topdelta <= prevdelta) - topdelta += prevdelta; - prevdelta = topdelta; - source = (const UINT8 *)(column) + 3; - - dest = desttop; - if (scrn & V_FLIP) - dest = deststart + (destend - dest); - topdelta = FixedInt(FixedMul(topdelta << FRACBITS, vdup)); - dest += topdelta * vid.width; - - if (scrn & V_VFLIP) - { - for (ofs = (column->length << FRACBITS)-1; dest < deststop && ofs >= 0; ofs -= rowfrac, ++offy) - { - if (clip != NULL) - { - const INT32 cy = y + topdelta - offy; - - if (cy < clip->top) // don't draw off the top of the clip rect - { - dest += vid.width; - continue; - } - - if (cy >= clip->bottom) // don't draw off the bottom of the clip rect - { - dest += vid.width; - continue; - } - } - - if (dest >= screens[scrn&V_SCREENMASK]) // don't draw off the top of the screen (CRASH PREVENTION) - *dest = patchdrawfunc(dest, source, ofs); - - dest += vid.width; - } - } - else - { - for (ofs = 0; dest < deststop && ofs < (column->length << FRACBITS); ofs += rowfrac, ++offy) - { - if (clip != NULL) - { - const INT32 cy = y + topdelta + offy; - - if (cy < clip->top) // don't draw off the top of the clip rect - { - dest += vid.width; - continue; - } - - if (cy >= clip->bottom) // don't draw off the bottom of the clip rect - { - dest += vid.width; - continue; - } - } - - if (dest >= screens[scrn&V_SCREENMASK]) // don't draw off the top of the screen (CRASH PREVENTION) - *dest = patchdrawfunc(dest, source, ofs); - - dest += vid.width; - } - } - - column = (const column_t *)((const UINT8 *)column + column->length + 4); - } + umin = 1.f - umin; + umax = 1.f - umax; } + if (scrn & V_VFLIP) + { + vmin = 1.f - vmin; + vmax = 1.f - vmax; + } + + if (alphalevel > 0 && alphalevel <= 10) + { + falpha = (10 - alphalevel) / 10.f; + } + hwr2::Draw2dBlend blend = hwr2::Draw2dBlend::kAlphaTransparent; + switch (blendmode) + { + case AST_MODULATE: + blend = hwr2::Draw2dBlend::kModulate; + break; + case AST_ADD: + blend = hwr2::Draw2dBlend::kAdditive; + break; + + // Note: SRB2 has these blend modes flipped compared to GL and Vulkan. + // SRB2's Subtract is Dst - Src. OpenGL is Src - Dst. And vice versa for reverse. + // Twodee will use the GL definitions. + case AST_SUBTRACT: + blend = hwr2::Draw2dBlend::kReverseSubtractive; + break; + case AST_REVERSESUBTRACT: + blend = hwr2::Draw2dBlend::kSubtractive; + break; + default: + blend = hwr2::Draw2dBlend::kAlphaTransparent; + break; + } + + auto builder = g_2d.begin_quad(); + builder + .patch(patch) + .rect(fx, fy, fx2 - fx, fy2 - fy) + .flip((scrn & V_FLIP) > 0) + .vflip((scrn & V_VFLIP) > 0) + .color(1, 1, 1, falpha) + .blend(blend) + .colormap(colormap); + + if (clip && clip->enabled) + { + builder.clip(clip->left, clip->top, clip->right, clip->bottom); + } + builder.done(); } // Draws a patch cropped and scaled to arbitrary size. @@ -1067,9 +1014,6 @@ void V_DrawBlock(INT32 x, INT32 y, INT32 scrn, INT32 width, INT32 height, const // void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c) { - UINT8 *dest; - const UINT8 *deststop; - if (rendermode == render_none) return; @@ -1087,18 +1031,20 @@ void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c) INT32 dupx = vid.dupx, dupy = vid.dupy; if (x == 0 && y == 0 && w == BASEVIDWIDTH && h == BASEVIDHEIGHT) - { // Clear the entire screen, from dest to deststop. Yes, this really works. - memset(screens[0], (c&255), vid.width * vid.height * vid.bpp); - return; + { + w = vid.width; + h = vid.height; } + else + { + x *= dupx; + y *= dupy; + w *= dupx; + h *= dupy; - x *= dupx; - y *= dupy; - w *= dupx; - h *= dupy; - - // Center it if necessary - V_AdjustXYWithSnap(&x, &y, c, dupx, dupy); + // Center it if necessary + V_AdjustXYWithSnap(&x, &y, c, dupx, dupy); + } } if (x >= vid.width || y >= vid.height) @@ -1122,13 +1068,18 @@ void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c) if (y + h > vid.height) h = vid.height - y; - dest = screens[0] + y*vid.width + x; - deststop = screens[0] + vid.rowbytes * vid.height; - c &= 255; - for (;(--h >= 0) && dest < deststop; dest += vid.width) - memset(dest, c, w * vid.bpp); + RGBA_t color = pMasterPalette[c]; + UINT8 r = (color.rgba & 0xFF); + UINT8 g = (color.rgba & 0xFF00) >> 8; + UINT8 b = (color.rgba & 0xFF0000) >> 16; + + g_2d.begin_quad() + .patch(nullptr) + .color(r / 255.f, g / 255.f, b / 255.f, 1.f) + .rect(x, y, w, h) + .done(); } #ifdef HWRENDER @@ -1169,10 +1120,6 @@ static UINT32 V_GetHWConsBackColor(void) void V_DrawFillConsoleMap(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c) { - UINT8 *dest; - const UINT8 *deststop; - INT32 u; - UINT8 *fadetable; UINT32 alphalevel = 0; if (rendermode == render_none) @@ -1231,37 +1178,18 @@ void V_DrawFillConsoleMap(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c) if (y + h > vid.height) h = vid.height-y; - dest = screens[0] + y*vid.width + x; - deststop = screens[0] + vid.rowbytes * vid.height; - c &= 255; - // Jimita (12-04-2018) - if (alphalevel) - { - fadetable = R_GetTranslucencyTable(alphalevel) + (c*256); - for (;(--h >= 0) && dest < deststop; dest += vid.width) - { - u = 0; - while (u < w) - { - dest[u] = fadetable[consolebgmap[dest[u]]]; - u++; - } - } - } - else - { - for (;(--h >= 0) && dest < deststop; dest += vid.width) - { - u = 0; - while (u < w) - { - dest[u] = consolebgmap[dest[u]]; - u++; - } - } - } + UINT32 hwcolor = V_GetHWConsBackColor(); + float r = ((hwcolor & 0xFF000000) >> 24) / 255.f; + float g = ((hwcolor & 0xFF0000) >> 16) / 255.f; + float b = ((hwcolor & 0xFF00) >> 8) / 255.f; + float a = 0.5f; // alphalevel is unused in GL?? + g_2d.begin_quad() + .rect(x, y, w, h) + .blend(hwr2::Draw2dBlend::kAlphaTransparent) + .color(r, g, b, a) + .done(); } // @@ -1273,9 +1201,7 @@ void V_DrawFillConsoleMap(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c) // void V_DrawDiag(INT32 x, INT32 y, INT32 wh, INT32 c) { - UINT8 *dest; - const UINT8 *deststop; - INT32 w, h, wait = 0; + INT32 w, h; if (rendermode == render_none) return; @@ -1321,7 +1247,6 @@ void V_DrawDiag(INT32 x, INT32 y, INT32 wh, INT32 c) return; // zero width/height wouldn't draw anything if (x + w > vid.width) { - wait = w - (vid.width - x); w = vid.width - x; } if (y + w > vid.height) @@ -1330,18 +1255,23 @@ void V_DrawDiag(INT32 x, INT32 y, INT32 wh, INT32 c) if (h > w) h = w; - dest = screens[0] + y*vid.width + x; - deststop = screens[0] + vid.rowbytes * vid.height; - c &= 255; - for (;(--h >= 0) && dest < deststop; dest += vid.width) { - memset(dest, c, w * vid.bpp); - if (wait) - wait--; - else - w--; + auto builder = g_2d.begin_verts(); + + const RGBA_t color = pMasterPalette[c]; + const float r = ((color.rgba & 0xFF000000) >> 24) / 255.f; + const float g = ((color.rgba & 0xFF0000) >> 16) / 255.f; + const float b = ((color.rgba & 0xFF00) >> 8) / 255.f; + const float a = 1.f; + builder.color(r, g, b, a); + + builder + .vert(x, y) + .vert(x + wh, y + wh) + .vert(x, y + wh) + .done(); } } @@ -1355,11 +1285,6 @@ void V_DrawDiag(INT32 x, INT32 y, INT32 wh, INT32 c) // void V_DrawFadeFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c, UINT16 color, UINT8 strength) { - UINT8 *dest; - const UINT8 *deststop; - INT32 u; - UINT8 *fadetable; - if (rendermode == render_none) return; @@ -1403,23 +1328,42 @@ void V_DrawFadeFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c, UINT16 color, U if (y + h > vid.height) h = vid.height-y; - dest = screens[0] + y*vid.width + x; - deststop = screens[0] + vid.rowbytes * vid.height; + float r; + float g; + float b; + float a; + hwr2::Draw2dBlend blendmode; - c &= 255; - - fadetable = ((color & 0xFF00) // Color is not palette index? - ? ((UINT8 *)colormaps + strength*256) // Do COLORMAP fade. - : ((UINT8 *)R_GetTranslucencyTable((9-strength)+1) + color*256)); // Else, do TRANSMAP** fade. - for (;(--h >= 0) && dest < deststop; dest += vid.width) + if (color & 0xFF00) { - u = 0; - while (u < w) - { - dest[u] = fadetable[dest[u]]; - u++; - } + // Historical COLORMAP fade + // In Ring Racers this is a Mega Drive style per-channel fade (though it'd probably be cool in SRB2 too) + // HWR2 will implement as a rev-subtractive rect because colormaps aren't possible in hardware + float fstrength = std::clamp(strength / 31.f, 0.f, 1.f); + r = std::clamp((fstrength - (0.f / 3.f)) * 3.f, 0.f, 1.f); + g = std::clamp((fstrength - (1.f / 3.f)) * 3.f, 0.f, 1.f); + b = std::clamp((fstrength - (2.f / 3.f)) * 3.f, 0.f, 1.f); + a = 1; + + blendmode = hwr2::Draw2dBlend::kReverseSubtractive; } + else + { + // Historically TRANSMAP fade + // This is done by modulative (transparent) blend to the given palette color. + byteColor_t bc = V_GetColor(color).s; + r = bc.red / 255.f; + g = bc.green / 255.f; + b = bc.blue / 255.f; + a = softwaretranstohwr[std::clamp(static_cast(strength), 0, 10)] / 255.f; + blendmode = hwr2::Draw2dBlend::kAlphaTransparent; + } + + g_2d.begin_quad() + .blend(blendmode) + .color(r, g, b, a) + .rect(x, y, w, h) + .done(); } // @@ -1427,11 +1371,10 @@ void V_DrawFadeFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c, UINT16 color, U // void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum) { - INT32 u, v, dupx, dupy; - fixed_t dx, dy, xfrac, yfrac; - const UINT8 *src, *deststop; - UINT8 *flat, *dest; - size_t size, lflatsize, flatshift; + INT32 dupx; + INT32 dupy; + size_t size; + size_t lflatsize; #ifdef HWRENDER if (rendermode == render_opengl) @@ -1440,89 +1383,52 @@ void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum) return; } #endif - size = W_LumpLength(flatnum); switch (size) { case 4194304: // 2048x2048 lump lflatsize = 2048; - flatshift = 11; break; case 1048576: // 1024x1024 lump lflatsize = 1024; - flatshift = 10; break; case 262144:// 512x512 lump lflatsize = 512; - flatshift = 9; break; case 65536: // 256x256 lump lflatsize = 256; - flatshift = 8; break; case 16384: // 128x128 lump lflatsize = 128; - flatshift = 7; break; case 1024: // 32x32 lump lflatsize = 32; - flatshift = 5; break; case 256: // 16x16 lump lflatsize = 16; - flatshift = 4; break; case 64: // 8x8 lump lflatsize = 8; - flatshift = 3; break; default: // 64x64 lump lflatsize = 64; - flatshift = 6; break; } - flat = static_cast(W_CacheLumpNum(flatnum, PU_CACHE)); + float fsize = lflatsize; dupx = dupy = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy); - dest = screens[0] + y*dupy*vid.width + x*dupx; - deststop = screens[0] + vid.rowbytes * vid.height; - - // from V_DrawScaledPatch - if (vid.width != BASEVIDWIDTH * dupx) - { - // dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx, - // so center this imaginary screen - dest += (vid.width - (BASEVIDWIDTH * dupx)) / 2; - } - if (vid.height != BASEVIDHEIGHT * dupy) - { - // same thing here - dest += (vid.height - (BASEVIDHEIGHT * dupy)) * vid.width / 2; - } - - w *= dupx; - h *= dupy; - - dx = FixedDiv(FRACUNIT, dupx<<(FRACBITS-2)); - dy = FixedDiv(FRACUNIT, dupy<<(FRACBITS-2)); - - yfrac = 0; - for (v = 0; v < h; v++, dest += vid.width) - { - xfrac = 0; - src = flat + (((yfrac>>FRACBITS) & (lflatsize - 1)) << flatshift); - for (u = 0; u < w; u++) - { - if (&dest[u] > deststop) - return; - dest[u] = src[(xfrac>>FRACBITS)&(lflatsize-1)]; - xfrac += dx; - } - yfrac += dy; - } + g_2d.begin_verts() + .flat(flatnum) + .vert(x * dupx, y * dupy, 0, 0) + .vert(x * dupx + w * dupx, y * dupy, w / fsize, 0) + .vert(x * dupx + w * dupx, y * dupy + h * dupy, w / fsize, h / fsize) + .vert(x * dupx, y * dupy, 0, 0) + .vert(x * dupx + w * dupx, y * dupy + h * dupy, w / fsize, h / fsize) + .vert(x * dupx, y * dupy + h * dupy, 0, h / fsize) + .done(); } // @@ -1619,21 +1525,42 @@ void V_DrawFadeScreen(UINT16 color, UINT8 strength) } #endif - { - const UINT8 *fadetable = - (color > 0xFFF0) // Grab a specific colormap palette? - ? R_GetTranslationColormap(color | 0xFFFF0000, static_cast(strength), GTC_CACHE) - : ((color & 0xFF00) // Color is not palette index? - ? ((UINT8 *)colormaps + strength*256) // Do COLORMAP fade. - : ((UINT8 *)R_GetTranslucencyTable((9-strength)+1) + color*256)); // Else, do TRANSMAP** fade. - const UINT8 *deststop = screens[0] + vid.rowbytes * vid.height; - UINT8 *buf = screens[0]; + float r; + float g; + float b; + float a; + hwr2::Draw2dBlend blendmode; - // heavily simplified -- we don't need to know x or y - // position when we're doing a full screen fade - for (; buf < deststop; ++buf) - *buf = fadetable[*buf]; + if (color & 0xFF00) + { + // Historical COLORMAP fade + // In Ring Racers this is a Mega Drive style per-channel fade (though it'd probably be cool in SRB2 too) + // HWR2 will implement as a rev-subtractive rect because colormaps aren't possible in hardware + float fstrength = std::clamp(strength / 31.f, 0.f, 1.f); + r = std::clamp((fstrength - (0.f / 3.f)) * 3.f, 0.f, 1.f); + g = std::clamp((fstrength - (1.f / 3.f)) * 3.f, 0.f, 1.f); + b = std::clamp((fstrength - (2.f / 3.f)) * 3.f, 0.f, 1.f); + a = 1; + + blendmode = hwr2::Draw2dBlend::kReverseSubtractive; } + else + { + // Historically TRANSMAP fade + // This is done by modulative (transparent) blend to the given palette color. + byteColor_t bc = V_GetColor(color).s; + r = bc.red / 255.f; + g = bc.green / 255.f; + b = bc.blue / 255.f; + a = softwaretranstohwr[std::clamp(static_cast(strength), 0, 10)] / 255.f; + blendmode = hwr2::Draw2dBlend::kAlphaTransparent; + } + + g_2d.begin_quad() + .blend(blendmode) + .color(r, g, b, a) + .rect(0, 0, vid.width, vid.height) + .done(); } // @@ -1651,6 +1578,7 @@ void V_DrawCustomFadeScreen(const char *lump, UINT8 strength) } #endif + // TODO: fix this for Twodee { lumpnum_t lumpnum = LUMPERROR; lighttable_t *clm = NULL; @@ -1686,22 +1614,24 @@ void V_DrawCustomFadeScreen(const char *lump, UINT8 strength) // Simple translucency with one color, over a set number of lines starting from the top. void V_DrawFadeConsBack(INT32 plines) { - UINT8 *deststop, *buf; - + UINT32 hwcolor = V_GetHWConsBackColor(); #ifdef HWRENDER // not win32 only 19990829 by Kin if (rendermode == render_opengl) { - UINT32 hwcolor = V_GetHWConsBackColor(); HWR_DrawConsoleBack(hwcolor, plines); return; } #endif - // heavily simplified -- we don't need to know x or y position, - // just the stop position - deststop = screens[0] + vid.rowbytes * std::min(plines, vid.height); - for (buf = screens[0]; buf < deststop; ++buf) - *buf = consolebgmap[*buf]; + float r = ((hwcolor & 0xFF000000) >> 24) / 255.f; + float g = ((hwcolor & 0xFF0000) >> 16) / 255.f; + float b = ((hwcolor & 0xFF00) >> 8) / 255.f; + float a = 0.5f; + g_2d.begin_quad() + .rect(0, 0, vid.width, plines) + .blend(hwr2::Draw2dBlend::kAlphaTransparent) + .color(r, g, b, a) + .done(); } @@ -1718,26 +1648,16 @@ void V_EncoreInvertScreen(void) } #endif - { - const UINT8 *deststop = screens[0] + vid.rowbytes * vid.height; - UINT8 *buf = screens[0]; - - for (; buf < deststop; ++buf) - { - *buf = NearestColor( - 255 - pLocalPalette[*buf].s.red, - 255 - pLocalPalette[*buf].s.green, - 255 - pLocalPalette[*buf].s.blue - ); - } - } + g_2d.begin_quad() + .blend(hwr2::Draw2dBlend::kInvertDest) + .color(1, 1, 1, 1) + .rect(0, 0, vid.width, vid.height) + .done(); } // Very similar to F_DrawFadeConsBack, except we draw from the middle(-ish) of the screen to the bottom. void V_DrawPromptBack(INT32 boxheight, INT32 color) { - UINT8 *deststop, *buf; - if (color >= 256 && color < 512) { if (boxheight < 0) @@ -1753,50 +1673,50 @@ void V_DrawPromptBack(INT32 boxheight, INT32 color) if (color == INT32_MAX) color = cons_backcolor.value; + UINT32 hwcolor; + switch (color) + { + case 0: hwcolor = 0xffffff00; break; // White + case 1: hwcolor = 0x00000000; break; // Black // Note this is different from V_DrawFadeConsBack + case 2: hwcolor = 0xdeb88700; break; // Sepia + case 3: hwcolor = 0x40201000; break; // Brown + case 4: hwcolor = 0xfa807200; break; // Pink + case 5: hwcolor = 0xff69b400; break; // Raspberry + case 6: hwcolor = 0xff000000; break; // Red + case 7: hwcolor = 0xffd68300; break; // Creamsicle + case 8: hwcolor = 0xff800000; break; // Orange + case 9: hwcolor = 0xdaa52000; break; // Gold + case 10: hwcolor = 0x80800000; break; // Yellow + case 11: hwcolor = 0x00ff0000; break; // Emerald + case 12: hwcolor = 0x00800000; break; // Green + case 13: hwcolor = 0x4080ff00; break; // Cyan + case 14: hwcolor = 0x4682b400; break; // Steel + case 15: hwcolor = 0x1e90ff00; break; // Periwinkle + case 16: hwcolor = 0x0000ff00; break; // Blue + case 17: hwcolor = 0xff00ff00; break; // Purple + case 18: hwcolor = 0xee82ee00; break; // Lavender + // Default green + default: hwcolor = 0x00800000; break; + } + #ifdef HWRENDER if (rendermode == render_opengl) { - UINT32 hwcolor; - switch (color) - { - case 0: hwcolor = 0xffffff00; break; // White - case 1: hwcolor = 0x00000000; break; // Black // Note this is different from V_DrawFadeConsBack - case 2: hwcolor = 0xdeb88700; break; // Sepia - case 3: hwcolor = 0x40201000; break; // Brown - case 4: hwcolor = 0xfa807200; break; // Pink - case 5: hwcolor = 0xff69b400; break; // Raspberry - case 6: hwcolor = 0xff000000; break; // Red - case 7: hwcolor = 0xffd68300; break; // Creamsicle - case 8: hwcolor = 0xff800000; break; // Orange - case 9: hwcolor = 0xdaa52000; break; // Gold - case 10: hwcolor = 0x80800000; break; // Yellow - case 11: hwcolor = 0x00ff0000; break; // Emerald - case 12: hwcolor = 0x00800000; break; // Green - case 13: hwcolor = 0x4080ff00; break; // Cyan - case 14: hwcolor = 0x4682b400; break; // Steel - case 15: hwcolor = 0x1e90ff00; break; // Periwinkle - case 16: hwcolor = 0x0000ff00; break; // Blue - case 17: hwcolor = 0xff00ff00; break; // Purple - case 18: hwcolor = 0xee82ee00; break; // Lavender - // Default green - default: hwcolor = 0x00800000; break; - } HWR_DrawTutorialBack(hwcolor, boxheight); return; } #endif - CON_SetupBackColormapEx(color, true); + float r = ((color & 0xFF000000) >> 24) / 255.f; + float g = ((color & 0xFF0000) >> 16) / 255.f; + float b = ((color & 0xFF00) >> 8) / 255.f; + float a = (color == 0 ? 0xC0 : 0x80) / 255.f; // make black darker, like software - // heavily simplified -- we don't need to know x or y position, - // just the start and stop positions - buf = deststop = screens[0] + vid.rowbytes * vid.height; - if (boxheight < 0) - buf += vid.rowbytes * boxheight; - else // 4 lines of space plus gaps between and some leeway - buf -= vid.rowbytes * ((boxheight * 4) + (boxheight/2)*5); - for (; buf < deststop; ++buf) - *buf = promptbgmap[*buf]; + INT32 real_boxheight = (boxheight * 4) + (boxheight / 2) * 5; + g_2d.begin_quad() + .rect(0, vid.height - real_boxheight, vid.width, real_boxheight) + .color(r, g, b, a) + .done(); } // Gets string colormap, used for 0x80 color codes diff --git a/src/v_video.h b/src/v_video.h index 497f0e712..bda5920d9 100644 --- a/src/v_video.h +++ b/src/v_video.h @@ -22,6 +22,14 @@ #include "hu_stuff.h" // fonts #ifdef __cplusplus + +#include "hwr2/twodee.hpp" + +namespace srb2 +{ +extern hwr2::Twodee g_2d; +} // namespace srb2 + extern "C" { #endif diff --git a/src/y_inter.c b/src/y_inter.c index 715289525..04b251ec5 100644 --- a/src/y_inter.c +++ b/src/y_inter.c @@ -336,9 +336,14 @@ void Y_IntermissionDrawer(void) { INT32 i, whiteplayer = MAXPLAYERS, x = 4, hilicol = highlightflags; + // If we early return, skip drawing the 3D scene (software buffer) so it doesn't clobber the frame for the wipe + g_wipeskiprender = true; + if (intertype == int_none || rendermode == render_none) return; + g_wipeskiprender = false; + // the merge was kind of a mess, how does this work -- toast 171021 { M_DrawMenuBackground(); @@ -810,7 +815,7 @@ void Y_StartIntermission(void) if (!timer) { // Prevent a weird bug - timer = 1; + timer = 1; } else if (nump < 2 && !netgame) { @@ -945,6 +950,9 @@ void Y_VoteDrawer(void) UINT8 selected[4]; fixed_t rubyheight = 0; + // If we early return, skip drawing the 3D scene (software buffer) so it doesn't clobber the frame for the wipe + g_wipeskiprender = true; + if (rendermode == render_none) return; @@ -954,6 +962,8 @@ void Y_VoteDrawer(void) if (!voteclient.loaded) return; + g_wipeskiprender = false; + { static angle_t rubyfloattime = 0; rubyheight = FINESINE(rubyfloattime>>ANGLETOFINESHIFT); diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 46e2b2a3d..2dd78d8c0 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -10,14 +10,11 @@ else() endif() if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}") -include("cpm-sdl2.cmake") + include("cpm-sdl2.cmake") include("cpm-zlib.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") @@ -26,9 +23,18 @@ 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") + +if (SRB2_CONFIG_ENABLE_WEBM_MOVIES) + if (NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}") + include("cpm-libvpx.cmake") + include("cpm-ogg.cmake") # libvorbis depends + include("cpm-libvorbis.cmake") + endif() + include("cpm-libwebm.cmake") + include("cpm-libyuv.cmake") +endif() add_subdirectory(tcbrindle_span) add_subdirectory(stb_vorbis) +add_subdirectory(stb_rect_pack) add_subdirectory(glad) diff --git a/thirdparty/cpm-imgui.cmake b/thirdparty/cpm-imgui.cmake index 2afd71a2c..788643f43 100644 --- a/thirdparty/cpm-imgui.cmake +++ b/thirdparty/cpm-imgui.cmake @@ -31,5 +31,6 @@ if(imgui_ADDED) target_include_directories(imgui PUBLIC "${imgui_BINARY_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/imgui_config") target_compile_definitions(imgui PUBLIC IMGUI_USER_CONFIG="srb2_imconfig.h") target_compile_features(imgui PUBLIC cxx_std_11) + target_link_libraries(imgui PRIVATE stb_rect_pack) add_library(imgui::imgui ALIAS imgui) endif() diff --git a/thirdparty/imgui_config/srb2_imconfig.h b/thirdparty/imgui_config/srb2_imconfig.h index 5c09001b2..48645d16c 100644 --- a/thirdparty/imgui_config/srb2_imconfig.h +++ b/thirdparty/imgui_config/srb2_imconfig.h @@ -5,6 +5,7 @@ #define IMGUI_DISABLE_OBSOLETE_FUNCTIONS #define IMGUI_DISABLE_OBSOLETE_KEYIO +#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION // We provide needed functionalities provided by default win32 impls through the interface layer #define IMGUI_DISABLE_WIN32_FUNCTIONS diff --git a/thirdparty/stb_rect_pack/CMakeLists.txt b/thirdparty/stb_rect_pack/CMakeLists.txt new file mode 100644 index 000000000..b0e610c95 --- /dev/null +++ b/thirdparty/stb_rect_pack/CMakeLists.txt @@ -0,0 +1,3 @@ +# Update from https://github.com/nothings/stb +add_library(stb_rect_pack STATIC stb_rect_pack.c include/stb_rect_pack.h) +target_include_directories(stb_rect_pack PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") diff --git a/thirdparty/stb_rect_pack/include/stb_rect_pack.h b/thirdparty/stb_rect_pack/include/stb_rect_pack.h new file mode 100644 index 000000000..6a633ce66 --- /dev/null +++ b/thirdparty/stb_rect_pack/include/stb_rect_pack.h @@ -0,0 +1,623 @@ +// stb_rect_pack.h - v1.01 - public domain - rectangle packing +// Sean Barrett 2014 +// +// Useful for e.g. packing rectangular textures into an atlas. +// Does not do rotation. +// +// Before #including, +// +// #define STB_RECT_PACK_IMPLEMENTATION +// +// in the file that you want to have the implementation. +// +// Not necessarily the awesomest packing method, but better than +// the totally naive one in stb_truetype (which is primarily what +// this is meant to replace). +// +// Has only had a few tests run, may have issues. +// +// More docs to come. +// +// No memory allocations; uses qsort() and assert() from stdlib. +// Can override those by defining STBRP_SORT and STBRP_ASSERT. +// +// This library currently uses the Skyline Bottom-Left algorithm. +// +// Please note: better rectangle packers are welcome! Please +// implement them to the same API, but with a different init +// function. +// +// Credits +// +// Library +// Sean Barrett +// Minor features +// Martins Mozeiko +// github:IntellectualKitty +// +// Bugfixes / warning fixes +// Jeremy Jaussaud +// Fabian Giesen +// +// Version history: +// +// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section +// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles +// 0.99 (2019-02-07) warning fixes +// 0.11 (2017-03-03) return packing success/fail result +// 0.10 (2016-10-25) remove cast-away-const to avoid warnings +// 0.09 (2016-08-27) fix compiler warnings +// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) +// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) +// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort +// 0.05: added STBRP_ASSERT to allow replacing assert +// 0.04: fixed minor bug in STBRP_LARGE_RECTS support +// 0.01: initial release +// +// LICENSE +// +// See end of file for license information. + +////////////////////////////////////////////////////////////////////////////// +// +// INCLUDE SECTION +// + +#ifndef STB_INCLUDE_STB_RECT_PACK_H +#define STB_INCLUDE_STB_RECT_PACK_H + +#define STB_RECT_PACK_VERSION 1 + +#ifdef STBRP_STATIC +#define STBRP_DEF static +#else +#define STBRP_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct stbrp_context stbrp_context; +typedef struct stbrp_node stbrp_node; +typedef struct stbrp_rect stbrp_rect; + +typedef int stbrp_coord; + +#define STBRP__MAXVAL 0x7fffffff +// Mostly for internal use, but this is the maximum supported coordinate value. + +STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); +// Assign packed locations to rectangles. The rectangles are of type +// 'stbrp_rect' defined below, stored in the array 'rects', and there +// are 'num_rects' many of them. +// +// Rectangles which are successfully packed have the 'was_packed' flag +// set to a non-zero value and 'x' and 'y' store the minimum location +// on each axis (i.e. bottom-left in cartesian coordinates, top-left +// if you imagine y increasing downwards). Rectangles which do not fit +// have the 'was_packed' flag set to 0. +// +// You should not try to access the 'rects' array from another thread +// while this function is running, as the function temporarily reorders +// the array while it executes. +// +// To pack into another rectangle, you need to call stbrp_init_target +// again. To continue packing into the same rectangle, you can call +// this function again. Calling this multiple times with multiple rect +// arrays will probably produce worse packing results than calling it +// a single time with the full rectangle array, but the option is +// available. +// +// The function returns 1 if all of the rectangles were successfully +// packed and 0 otherwise. + +struct stbrp_rect +{ + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + +}; // 16 bytes, nominally + + +STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); +// Initialize a rectangle packer to: +// pack a rectangle that is 'width' by 'height' in dimensions +// using temporary storage provided by the array 'nodes', which is 'num_nodes' long +// +// You must call this function every time you start packing into a new target. +// +// There is no "shutdown" function. The 'nodes' memory must stay valid for +// the following stbrp_pack_rects() call (or calls), but can be freed after +// the call (or calls) finish. +// +// Note: to guarantee best results, either: +// 1. make sure 'num_nodes' >= 'width' +// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' +// +// If you don't do either of the above things, widths will be quantized to multiples +// of small integers to guarantee the algorithm doesn't run out of temporary storage. +// +// If you do #2, then the non-quantized algorithm will be used, but the algorithm +// may run out of temporary storage and be unable to pack some rectangles. + +STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); +// Optionally call this function after init but before doing any packing to +// change the handling of the out-of-temp-memory scenario, described above. +// If you call init again, this will be reset to the default (false). + + +STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); +// Optionally select which packing heuristic the library should use. Different +// heuristics will produce better/worse results for different data sets. +// If you call init again, this will be reset to the default. + +enum +{ + STBRP_HEURISTIC_Skyline_default=0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight +}; + + +////////////////////////////////////////////////////////////////////////////// +// +// the details of the following structures don't matter to you, but they must +// be visible so you can handle the memory allocations for them + +struct stbrp_node +{ + stbrp_coord x,y; + stbrp_node *next; +}; + +struct stbrp_context +{ + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' +}; + +#ifdef __cplusplus +} +#endif + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +#ifdef _MSC_VER +#define STBRP__NOTUSED(v) (void)(v) +#define STBRP__CDECL __cdecl +#else +#define STBRP__NOTUSED(v) (void)sizeof(v) +#define STBRP__CDECL +#endif + +enum +{ + STBRP__INIT_skyline = 1 +}; + +STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) +{ + switch (context->init_mode) { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } +} + +STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) +{ + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes-1) / context->num_nodes; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; + + for (i=0; i < num_nodes-1; ++i) + nodes[i].next = &nodes[i+1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; + context->extra[1].y = (1<<30); + context->extra[1].next = NULL; +} + +// find minimum y position if it starts at x1 +static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) +{ + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + + STBRP__NOTUSED(c); + + STBRP_ASSERT(first->x <= x0); + + #if 0 + // skip in case we're past the node + while (node->next->x <= x0) + ++node; + #else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency + #endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) { + if (node->y > min_y) { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } else { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; +} + +typedef struct +{ + int x,y; + stbrp_node **prev_link; +} stbrp__findresult; + +static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) +{ + int best_waste = (1<<30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + // if it can't possibly fit, bail immediately + if (width > c->width || height > c->height) { + fr.prev_link = NULL; + fr.x = fr.y = 0; + return fr; + } + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) { + int y,waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL + // bottom left + if (y < best_y) { + best_y = y; + best = prev; + } + } else { + // best-fit + if (y + height <= c->height) { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) { + int xpos = tail->x - width; + int y,waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height <= c->height) { + if (y <= best_y) { + if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { + best_x = xpos; + STBRP_ASSERT(y <= best_y); + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; +} + +static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) +{ + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } else { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); + +#ifdef _DEBUG + cur = context->active_head; + while (cur->x < context->width) { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + int count=0; + cur = context->active_head; + while (cur) { + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) { + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes+2); + } +#endif + + return res; +} + +static int STBRP__CDECL rect_height_compare(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); +} + +static int STBRP__CDECL rect_original_order(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); +} + +STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) +{ + int i, all_rects_packed = 1; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = i; + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i=0; i < num_rects; ++i) { + if (rects[i].w == 0 || rects[i].h == 0) { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } else { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } else { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags and all_rects_packed status + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); + if (!rects[i].was_packed) + all_rects_packed = 0; + } + + // return the all_rects_packed status + return all_rects_packed; +} +#endif + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/thirdparty/stb_rect_pack/stb_rect_pack.c b/thirdparty/stb_rect_pack/stb_rect_pack.c new file mode 100644 index 000000000..3f3391d6f --- /dev/null +++ b/thirdparty/stb_rect_pack/stb_rect_pack.c @@ -0,0 +1,2 @@ +#define STB_RECT_PACK_IMPLEMENTATION +#include "include/stb_rect_pack.h"