#include "OcclusionDebug.h"
#include <map>
#include <set>
#include <mutex>
#include <vector>
#include <string>
#include <imgui.h>
#include <libultraship/bridge.h>
#include <ship/Context.h>
#include <spdlog/spdlog.h>
#include "port/UI/cvar_prefixes.h"
#include "port/Patches/GeoCull.h"
#include "port/ShipInit.hpp"
#include "port/Enhancements/Events/Hooks/Events.h"
#include "functions.h"
#define CVAR_OCCLUSION_ACTIVE CVAR_DEVELOPER_TOOLS("OcclusionDebug.Active")
namespace {
constexpr int kParts = 3;
const char* kPartNames[kParts] = { "OPA", "XLU", "?" };
const char* CmdTypeName(int type) {
switch (type) {
case OCCLUSION_CMD_CAMERA:
return "CAMERA";
case OCCLUSION_CMD_LOD:
return "LOD";
case OCCLUSION_CMD_UNKE:
return "UnkE";
default:
return "?";
}
}
struct Key {
int part;
int offset;
bool operator<(const Key& o) const {
return (part != o.part) ? (part < o.part) : (offset < o.offset);
}
};
struct CullCmdRecord {
int type;
int areaCount;
unsigned char areaIds[12];
int detail0;
int detail1;
int drawnVanilla;
bool seenThisFrame;
};
std::mutex sMutex;
std::map<Key, CullCmdRecord> sRecorded;
std::set<Key> sForceDraw;
bool sForceAll = false;
int sRecordedMap = -1;
int PartForBin(const void* bin) {
if (bin == (const void*)mapModel_getModelBin(0)) {
return 0;
}
if (bin == (const void*)mapModel_getModelBin(1)) {
return 1;
}
return 2;
}
std::string DetailString(const CullCmdRecord& rec) {
if (rec.type == OCCLUSION_CMD_CAMERA) {
std::string ids;
for (int i = 0; i < rec.areaCount; i++) {
if (i) {
ids += ",";
}
ids += std::to_string((int)rec.areaIds[i]);
}
const char* branch =
(rec.detail0 & 1) ? ((rec.detail0 & 2) ? "in+out" : "outside") : ((rec.detail0 & 2) ? "inside" : "-");
return "areas {" + ids + "} " + branch;
}
if (rec.type == OCCLUSION_CMD_LOD) {
return "dist " + std::to_string(rec.detail0) + ".." + std::to_string(rec.detail1);
}
if (rec.type == OCCLUSION_CMD_UNKE) {
return "frustum sphere";
}
return "";
}
void OnGeoCull_Record(IEvent* event) {
auto* ev = reinterpret_cast<OnGeoCull*>(event);
Key key{ PartForBin(ev->modelBin), ev->offset };
std::lock_guard<std::mutex> lock(sMutex);
CullCmdRecord& rec = sRecorded[key];
rec.type = ev->type;
rec.areaCount = (ev->areaCount > 12) ? 12 : (ev->areaCount < 0 ? 0 : ev->areaCount);
for (int i = 0; i < 12; i++) {
rec.areaIds[i] = (i < rec.areaCount && ev->areaIds != nullptr) ? ev->areaIds[i] : 0;
}
rec.detail0 = ev->detail0;
rec.detail1 = ev->detail1;
rec.drawnVanilla = ev->drawnVanilla;
rec.seenThisFrame = true;
if (sForceAll || sForceDraw.count(key)) {
*ev->forceDraw = true;
}
}
void OnFrame_Reset(IEvent*) {
std::lock_guard<std::mutex> lock(sMutex);
int map = (int)gsworld_getMap();
if (map != sRecordedMap) {
sRecorded.clear();
sForceDraw.clear();
sRecordedMap = map;
}
for (auto& [key, rec] : sRecorded) {
rec.seenThisFrame = false;
}
}
}
void RegisterOcclusionDebug_Init() {
bool active = CVarGetInteger(CVAR_OCCLUSION_ACTIVE, 0);
GeoCull_SetConsumer(GEOCULL_CONSUMER_DEBUG, active);
COND_HOOK(OnGeoCull, EVENT_PRIORITY_NORMAL, active, OnGeoCull_Record);
COND_HOOK(GameFrameUpdate, EVENT_PRIORITY_NORMAL, active, OnFrame_Reset);
}
static RegisterShipInitFunc sInitOcclusionDebug(RegisterOcclusionDebug_Init, { CVAR_OCCLUSION_ACTIVE });
void OcclusionDebugWindow::DrawElement() {
bool active = CVarGetInteger(CVAR_OCCLUSION_ACTIVE, 0);
if (ImGui::Checkbox("Enable recording / force-draw", &active)) {
CVarSetInteger(CVAR_OCCLUSION_ACTIVE, active);
Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
ImGui::TextWrapped(
"Flip 'Force draw all' on for a moment to walk every cull command and populate the list (the "
"screen will look broken - that is expected), then turn it off and tick 'Force Draw' on "
"individual rows to find the chunk(s) that reveal the target scenery. Offsets are stable, so the "
"list never shifts. Dump the chosen set to the log when done.");
struct Row {
Key key;
CullCmdRecord rec;
bool forced;
};
std::vector<Row> rows;
bool forceAll;
int mapId;
{
std::lock_guard<std::mutex> lock(sMutex);
forceAll = sForceAll;
mapId = sRecordedMap;
rows.reserve(sRecorded.size());
for (auto& [key, rec] : sRecorded) {
rows.push_back({ key, rec, sForceDraw.count(key) > 0 });
}
}
ImGui::Separator();
ImGui::Text("Map: 0x%X Cull commands seen: %zu", mapId, rows.size());
if (ImGui::Checkbox("Force draw all (enumerate)", &forceAll)) {
std::lock_guard<std::mutex> lock(sMutex);
sForceAll = forceAll;
}
ImGui::SameLine();
if (ImGui::Button("Clear list")) {
std::lock_guard<std::mutex> lock(sMutex);
sRecorded.clear();
sRecordedMap = -1;
}
ImGui::SameLine();
if (ImGui::Button("Clear force-draw")) {
std::lock_guard<std::mutex> lock(sMutex);
sForceDraw.clear();
}
ImGui::SameLine();
if (ImGui::Button("Dump force-draw set to log")) {
std::string parts[kParts];
{
std::lock_guard<std::mutex> lock(sMutex);
for (const Key& k : sForceDraw) {
if (k.part < 0 || k.part >= kParts) {
continue;
}
char buf[16];
snprintf(buf, sizeof(buf), "0x%X", k.offset);
if (!parts[k.part].empty()) {
parts[k.part] += ", ";
}
parts[k.part] += buf;
}
}
SPDLOG_INFO("[OcclusionDebug] map 0x{:X}: OPA {{{}}} XLU {{{}}}", mapId, parts[0], parts[1]);
}
if (ImGui::BeginTable("OcclusionCmds", 6,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit |
ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("Part", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Offset", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Detail", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Drawn", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Force Draw", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableHeadersRow();
for (Row& row : rows) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%s", kPartNames[row.key.part]);
ImGui::TableNextColumn();
ImGui::Text("0x%X", row.key.offset);
ImGui::TableNextColumn();
ImGui::Text("%s", CmdTypeName(row.rec.type));
ImGui::TableNextColumn();
ImGui::TextWrapped("%s", DetailString(row.rec).c_str());
ImGui::TableNextColumn();
if (!row.rec.seenThisFrame) {
ImGui::TextDisabled("--");
} else {
ImGui::Text("%s", row.rec.drawnVanilla ? "yes" : "no");
}
ImGui::TableNextColumn();
bool fd = row.forced;
std::string id = "##fd" + std::to_string(row.key.part) + "_" + std::to_string(row.key.offset);
if (ImGui::Checkbox(id.c_str(), &fd)) {
std::lock_guard<std::mutex> lock(sMutex);
if (fd) {
sForceDraw.insert(row.key);
} else {
sForceDraw.erase(row.key);
}
}
}
ImGui::EndTable();
}
}