Skip to content

Commit

Permalink
DrawDistance: separate code into hooks_drawdistance.cpp
Browse files Browse the repository at this point in the history
  • Loading branch information
emoose committed Dec 8, 2024
1 parent 14fb009 commit 9232871
Show file tree
Hide file tree
Showing 3 changed files with 357 additions and 347 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ set(outrun2006tweaks_SOURCES
"src/hook_mgr.hpp"
"src/hooks_audio.cpp"
"src/hooks_bugfixes.cpp"
"src/hooks_drawdistance.cpp"
"src/hooks_exceptions.cpp"
"src/hooks_flac.cpp"
"src/hooks_forcefeedback.cpp"
Expand Down
355 changes: 355 additions & 0 deletions src/hooks_drawdistance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
#include "hook_mgr.hpp"
#include "plugin.hpp"
#include "game_addrs.hpp"
#include <array>
#include <bitset>
#include <imgui.h>

std::array<std::vector<uint16_t>, 256> ObjectNodes;
std::array<std::array<std::bitset<16384>, 256>, 128> ObjectExclusionsPerStage;
int NumObjects = 0;

bool DrawDistanceIncreaseEnabled = false;
bool EnablePauseMenu = true;

void Overlay_DrawDistOverlay()
{
uint32_t cur_stage_num = Game::GetStageUniqueNum(Game::GetNowStageNum(8));
auto& objectExclusions = ObjectExclusionsPerStage[cur_stage_num];

ImGui::Begin("Draw Distance Debugger");

ImGui::Checkbox("Countdown timer enabled", Game::Sumo_CountdownTimerEnable);
ImGui::Checkbox("Pause menu enabled", &EnablePauseMenu);

// get max column count
int num_columns = 0;
for (int i = 0; i < NumObjects; i++)
{
size_t size = ObjectNodes[i].size();
if (size > num_columns)
num_columns = size;
}

static ImGuiTableFlags table_flags = ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_HighlightHoveredColumn;

ImGui::Text("Usage:");
ImGui::Text("- If an ugly LOD object appears, pause game with ESC and press F11 to bring up this window");
ImGui::Text("- Reduce Draw Distance below to lowest value that still has the LOD object appearing");
ImGui::Text("- Once you find the draw-distance that shows the object, click each node checkbox to disable nodes");
ImGui::Text("- After finding the node responsible, you can use \"Copy to clipboard\" below to copy the IDs of them, or hover over the node");
ImGui::Text("- Post the IDs for LODs you find in the \"DrawDistanceIncrease issue reports\" github thread and we can add exclusions for them!");
ImGui::NewLine();

if (!DrawDistanceIncreaseEnabled)
{
ImGui::Text("Error: DrawDistanceIncrease must be enabled in INI before launching.");
ImGui::End();
return;
}

ImGui::SliderInt("Draw Distance", &Settings::DrawDistanceIncrease, 0, 1024);
if (ImGui::Button("<<<"))
Settings::DrawDistanceIncrease--;
ImGui::SameLine();
if (ImGui::Button(">>>"))
Settings::DrawDistanceIncrease++;

if (num_columns > 0)
{
ImGui::Text("Nodes at DrawDistance %d:", Settings::DrawDistanceIncrease);

num_columns += 1;
if (ImGui::BeginTable("NodeTable", num_columns, table_flags))
{
ImGui::TableSetupColumn("Object ID", ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_NoReorder);
for (int n = 1; n < num_columns; n++)
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);

for (int objectIdx = 0; objectIdx < NumObjects; objectIdx++)
{
ImGui::PushID(objectIdx);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (ImGui::Button(std::format("Object {}", objectIdx).c_str()))
{
bool areAllExcluded = true;
for (int i = 0; i < ObjectNodes[objectIdx].size(); i++)
{
auto nodeId = ObjectNodes[objectIdx][i];
if (!objectExclusions[objectIdx][nodeId])
{
areAllExcluded = false;
break;
}
}

for (int i = 0; i < ObjectNodes[objectIdx].size(); i++)
{
auto nodeId = ObjectNodes[objectIdx][i];
objectExclusions[objectIdx][nodeId] = areAllExcluded ? false : true;
}
}

for (int i = 0; i < ObjectNodes[objectIdx].size(); i++)
if (ImGui::TableSetColumnIndex(i + 1))
{
ImGui::PushID(i + 1);

auto nodeId = ObjectNodes[objectIdx][i];
bool excluded = objectExclusions[objectIdx][nodeId];

if (ImGui::Checkbox("", &excluded))
objectExclusions[objectIdx][nodeId] = excluded;

ImGui::SetItemTooltip("Stage %d, object 0x%X, node 0x%X", cur_stage_num, objectIdx, nodeId);

ImGui::PopID();
}

ImGui::PopID();
}
ImGui::EndTable();
}
}

if (ImGui::Button("Copy exclusions to clipboard"))
{
std::string clipboard = "";//
for (int objId = 0; objId < objectExclusions.size(); objId++)
{
std::string objLine = "";
for (int i = 0; i < objectExclusions[objId].size(); i++)
{
if (objectExclusions[objId][i])
{
objLine += std::format(", 0x{:X}", i);
}
}

if (objLine.length() > 2)
{
clipboard += std::format("\n0x{:X} = {}", objId, objLine.substr(2));
}
}
if (!clipboard.empty())
clipboard = std::format("[Stage {}]{}", cur_stage_num, clipboard);

HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, clipboard.length() + 1);
if (hMem)
{
memcpy(GlobalLock(hMem), clipboard.c_str(), clipboard.length() + 1);
GlobalUnlock(hMem);
OpenClipboard(0);
EmptyClipboard();
SetClipboardData(CF_TEXT, hMem);
CloseClipboard();
}
}

static bool showReallyPrompt = false;
if (ImGui::Button(showReallyPrompt ? "Are you sure?" : "Clear all exclusions"))
{
if (!showReallyPrompt)
{
showReallyPrompt = true;
}
else
{
for (int i = 0; i < objectExclusions.size(); i++)
objectExclusions[i].reset();
showReallyPrompt = false;
}
}

ImGui::End();
}

class DrawDistanceIncrease : public Hook
{
// Stage drawing/culling is based on the player cars position in the stage
// Each ~1m of track has an ID, which indexes data inside the cs_CS_[stg]_bin.sz file
// This data contains a list of CullingNode indexes pointing to matrices and model pointers for drawing
//
// The first attempt to increase draw distance looped the existing draw code, incrementing the track ID up to a specified max-distance-increase variable
// This seemed effective but would cause some darker stage shadowing and duplicate sprites to show
// Appears that some of the track-ID lists shared CullingNode IDs inside them, leading to models being drawn multiple times
//
// After learning more about the CullingNode setup, a second attempt instead tried to loop over each CullingNode list and add unique entries to a list
// (via a statically allocated unique-CullingNode-ID array and std::bitset to track seen IDs)
// With this the distance could be increased without the duplicate model issues, and without affecting performance too heavily
//
// ---
//
// There are a couple issues with this setup right now though, fortunately they only really appear when distance is increased quite far (>64):
//
// - Some CullingNode lists meant for later in the stage may contain LOD models for earlier parts
// eg. Palm Beach has turns later on where LOD models for the beginning section appear (sometimes overwriting the non-LOD models somehow?)
// Some kind of filter-list that defines those LOD models and only allows the default csOffset = 0 to draw them might help this
//
// - Maps like Canyon with vertical progression can show higher-up parts of the track early, which vanilla game wouldn't display until they were close by.
// With increased draw distance, those sections then appear in the sky without any connecting textures...
// May need to add per-stage / per-section distance limits to workaround this
//
// - Increasing DrawDistanceBehind and using a camera mod to view behind the car reveals backface-culling issues, since those faces were never visible in the vanilla game.
// Wonder how the attract videos for the game were able to use other camera angles without any backface-culling showing up...
// Those videos seem to be captured in-engine, were they using special versions of the map that included all faces?
// (or possibly all the faces are already included in the current maps, and it's something CullingNode-related which skips drawing them?
// I'm not hopeful about that though, doubt they would have included data for parts that wouldn't be shown)

// Known bad CullingNode IDs:
// Palm Beach
// (obj4,node0xF1) = breaks railings at the beginning
// Metropolis
// ??? = breaks trees

inline static uint16_t CollisionNodeIdxArray[4096];
inline static std::bitset<4096> CollisionNodesToDisplay;

inline static SafetyHookMid dest_hook = {};
static void destination(safetyhook::Context& ctx)
{
int xmtSetShifted = *(int*)(ctx.esp + 0x14); // XMTSET num shifted left by 16
uint8_t* a2 = *(uint8_t**)(ctx.esp + 0x24);
int a4 = *(int*)(a2 + 0x70);
int a5 = ctx.edx;

int CsMaxLength = Game::GetMaxCsLen(0);
int CsLengthNum = ctx.ebp;

int v6 = ctx.ebx;
uint32_t* v11 = (uint32_t*)(v6 + 8);

uint32_t cur_stage_num = Game::GetStageUniqueNum(Game::GetNowStageNum(8));
auto& objectExclusions = ObjectExclusionsPerStage[cur_stage_num];

NumObjects = *(int*)(ctx.esp + 0x18);
for (int ObjectNum = 0; ObjectNum < NumObjects; ObjectNum++)
{
CollisionNodesToDisplay.reset();
uint16_t* cur = CollisionNodeIdxArray;

for (int csOffset = -Settings::DrawDistanceBehind; csOffset < (Settings::DrawDistanceIncrease + 1); csOffset++)
{
if (csOffset != 0)
{
// If current offset is below idx 0 skip to next one
if (CsLengthNum + csOffset < 0)
continue;

// If we're past the max entries for the stage then break out
if (CsLengthNum + csOffset >= (CsMaxLength - 1))
break;
}

// DEBUG: clear lastadds for this objectnum here
if (Settings::OverlayEnabled && csOffset == Settings::DrawDistanceIncrease)
{
ObjectNodes[ObjectNum].clear();
}

uint32_t sectionCollListOffset = *(uint32_t*)(v6 + *v11 + ((CsLengthNum + csOffset) * 4));
uint16_t* sectionCollList = (uint16_t*)(v6 + *v11 + sectionCollListOffset);

int num = 0;
while (*sectionCollList != 0xFFFF)
{
// If we haven't seen this CollisionNode idx already lets add it to our IdxArray
if (!CollisionNodesToDisplay[*sectionCollList])
{
CollisionNodesToDisplay[*sectionCollList] = true;

// DEBUG: check exclusions here before adding to *cur
// (if we're at csOffset = 0, exclusions are ignored, since that is what vanilla game would display)
if (!Settings::OverlayEnabled || !objectExclusions[ObjectNum][*sectionCollList] || csOffset == 0)
{
*cur = *sectionCollList;
cur++;
}

// DEBUG: add *sectionCollList to lastadds list here
if (Settings::OverlayEnabled && csOffset == Settings::DrawDistanceIncrease)
ObjectNodes[ObjectNum].push_back(*sectionCollList);

num++;
}

sectionCollList++;
}
}

*cur = 0xFFFF;

Game::DrawObject_Internal(xmtSetShifted | ObjectNum, 0, CollisionNodeIdxArray, a4, a5, 0);

v11++;
}
}

public:
std::string_view description() override
{
return "DrawDistanceIncrease";
}

bool validate() override
{
return Settings::DrawDistanceIncrease > 0 || Settings::DrawDistanceBehind > 0;
}

bool apply() override
{
constexpr int DispStage_HookAddr = 0x4DF6D;

Memory::VP::Nop(Module::exe_ptr(DispStage_HookAddr), 0x4B);
dest_hook = safetyhook::create_mid(Module::exe_ptr(DispStage_HookAddr), destination);

for (int i = 0; i < ObjectNodes.size(); i++)
ObjectNodes[i].reserve(4096);

DrawDistanceIncreaseEnabled = true;

return true;
}

static DrawDistanceIncrease instance;
};
DrawDistanceIncrease DrawDistanceIncrease::instance;

class PauseMenuVisibility : public Hook
{
inline static SafetyHookInline sprani_hook = {};
static void sprani_dest()
{
if (EnablePauseMenu)
sprani_hook.call();
}

inline static SafetyHookInline pauseframedisp_hook = {};
static void __fastcall pauseframedisp_dest(void* thisptr, void* unused)
{
if (EnablePauseMenu)
pauseframedisp_hook.thiscall(thisptr);
}

public:
std::string_view description() override
{
return "PauseMenuVisibility";
}

bool validate() override
{
return Settings::OverlayEnabled;
}

bool apply() override
{
sprani_hook = safetyhook::create_inline(Module::exe_ptr(0x28170), sprani_dest);
pauseframedisp_hook = safetyhook::create_inline(Module::exe_ptr(0x8C5F0), pauseframedisp_dest);
return true;
}

static PauseMenuVisibility instance;
};
PauseMenuVisibility PauseMenuVisibility::instance;
Loading

0 comments on commit 9232871

Please sign in to comment.