exe icon from the player's own game files at runtime; close-clone prop jitter fix

This commit is contained in:
mchughalex 2026-07-11 09:29:02 +01:00
parent 4776a8d514
commit b99dbc400d
5 changed files with 379 additions and 3 deletions

View file

@ -234,6 +234,7 @@ set(SKATE3_COMMON_SOURCES
src/skate3_shader_disasm.cpp
src/skate3_title_update_installer.cpp
src/skate3_user_settings.cpp
src/skate3_win_icon.cpp
third_party/rexglue-sdk/thirdparty/crypto/sha256.cpp
)
if(APPLE)

View file

@ -7,6 +7,7 @@
#include "skate3_native_scene.h"
#include "skate3_screenshot.h"
#include "skate3_shader_disasm.h"
#include "skate3_win_icon.h"
#include "skate3_title_update_installer.h"
#include "skate3_user_settings.h"
@ -687,6 +688,15 @@ std::optional<rex::PathConfig> Skate3BaseApp::OnFinalizePaths(
}
runtime_paths = std::move(tu_paths);
}
#endif
#if defined(_WIN32)
// Window/taskbar + Explorer icon sourced from the user's OWN game art at
// runtime (game/nxeart); the shipped exe and the repo carry no EA
// artwork (see skate3_win_icon.h). Game files are guaranteed installed by
// this point on Windows (the install wizards above run blocking).
skate3::ApplyGameIconFromGameData(
runtime_paths.game_data_root,
window() ? window()->GetNativeWindowHandle() : nullptr);
#endif
return runtime_paths;
}

View file

@ -5332,7 +5332,8 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
};
uint64_t key = (uint64_t(item.mesh) << 8) | (k & 0xFF);
DynHist* hp = &s_hist[key];
if (hp->seen == s_frame || hist_dist2(*hp) > 2.25f) {
const float own_d2 = hp->seen == s_frame ? 1e30f : hist_dist2(*hp);
if (own_d2 > 1e-4f) {
// The k-th slot mispairs: the game's sort lists RESHUFFLE same-mesh
// clones as they and the camera move. For static props the
// reset-on-jump guard below was enough (a mispair rendered raw for a
@ -5341,7 +5342,24 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
// stepped poses (the vehicle judder/catch-up). Re-pair by POSITION
// instead: claim the unclaimed history of this mesh whose newest
// pose is nearest, within the same 1.5 m one-tick jump gate.
float best = 2.25f;
// The search runs whenever the slot is not an (almost) EXACT
// continuation, not only past the 1.5 m gate: clone placements
// CLOSER than 1.5 m (the paired newspaper holders, 0.78 m apart)
// otherwise inherit each other's history on every reshuffle, and the
// interpolator renders them sliding between the two placements ("the
// props jitter in and out of position"). Seeding `best` with the own
// slot's distance keeps a claim strictly-nearer-only, so a genuinely
// moving entity still prefers its own ring.
// RIGID claims get a far tighter cap than the vehicles' 1.5 m: when a
// close clone's OWN ring goes stale (its submission flapped for a few
// frames), strictly-nearer alone still claims the TWIN's fresh ring
// (own = 1e30) and slides once, the residual single jitter seen
// after the strictly-nearer fix. No rigid prop moves 0.3 m in one sim
// tick (18 m/s at 60 Hz), while clone placements sit >= 0.78 m apart;
// a rigid item with no history inside 0.3 m starts a fresh ring and
// renders raw at its correct placement instead.
const float claim_cap = skinned ? 2.25f : 0.09f;
float best = std::min(own_d2, claim_cap);
DynHist* alt = nullptr;
uint64_t alt_key = key;
uint32_t fresh_k = 256; // first index past the dense key range
@ -5596,7 +5614,23 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
const float dz = item.world[14] - latest.w[14];
d2 = dx * dx + dy * dy + dz * dz;
}
discontinuity = d2 > 2.25f; // > 1.5 m
// Rigid uses the same tight one-tick bound as the claim cap
// above: 0.78 m clone placements sit inside the vehicles' 1.5 m
// gate, and lerping across a mispair IS the prop jitter.
discontinuity = d2 > (skinned ? 2.25f : 0.09f);
if (discontinuity && !skinned && d2 < 2.25f) {
// A rigid step that only the tightened bound caught = a
// close-clone mispair that would have LERPED (the prop jitter).
static std::atomic<uint64_t> s_rigid_mispair{0};
const uint64_t n =
s_rigid_mispair.fetch_add(1, std::memory_order_relaxed);
if (n < 24 || (n & 255u) == 0) {
REXLOG_INFO(
"native-scene: rigid close-clone mispair reset mesh={:08X} "
"k={} d={:.2f}m (n={})",
item.mesh, k, std::sqrt(d2), n);
}
}
}
if (discontinuity) {
h.count = 0;

307
src/skate3_win_icon.cpp Normal file
View file

@ -0,0 +1,307 @@
// Application icon from the user's own game files, see skate3_win_icon.h.
#include "skate3_win_icon.h"
#if defined(_WIN32)
#include <windows.h>
// initguid must precede wincodec so the WIC CLSIDs/IIDs are defined in this
// translation unit (no dependency on uuid.lib link order).
#include <initguid.h>
#include <wincodec.h>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <thread>
#include <vector>
#include <rex/logging.h>
namespace skate3 {
namespace {
// First embedded PNG in the nxeart dashboard-art package = the 64x64 title
// tile (two identical copies exist; either is fine).
std::vector<uint8_t> ExtractFirstPng(const std::filesystem::path& path) {
std::ifstream f(path, std::ios::binary);
if (!f) {
return {};
}
std::vector<uint8_t> data((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
static const uint8_t kMagic[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
static const uint8_t kIend[4] = {'I', 'E', 'N', 'D'};
for (size_t i = 0; i + 8 <= data.size(); ++i) {
if (std::memcmp(data.data() + i, kMagic, 8) != 0) {
continue;
}
for (size_t j = i + 8; j + 8 <= data.size(); ++j) {
if (std::memcmp(data.data() + j, kIend, 4) == 0) {
// IEND chunk data end + 4-byte CRC.
return std::vector<uint8_t>(data.begin() + i, data.begin() + j + 8);
}
}
break;
}
return {};
}
// Decode the PNG and scale to `size` x `size` 32bpp BGRA (top-down rows).
bool WicDecodeScaled(IWICImagingFactory* wic, const std::vector<uint8_t>& png,
uint32_t size, std::vector<uint8_t>& bgra_out) {
IWICStream* stream = nullptr;
if (FAILED(wic->CreateStream(&stream))) {
return false;
}
bool ok = false;
IWICBitmapDecoder* decoder = nullptr;
IWICBitmapFrameDecode* frame = nullptr;
IWICBitmapScaler* scaler = nullptr;
IWICFormatConverter* converter = nullptr;
do {
if (FAILED(stream->InitializeFromMemory(
const_cast<BYTE*>(png.data()), DWORD(png.size())))) {
break;
}
if (FAILED(wic->CreateDecoderFromStream(stream, nullptr,
WICDecodeMetadataCacheOnDemand,
&decoder))) {
break;
}
if (FAILED(decoder->GetFrame(0, &frame))) {
break;
}
if (FAILED(wic->CreateBitmapScaler(&scaler)) ||
FAILED(scaler->Initialize(frame, size, size,
WICBitmapInterpolationModeFant))) {
break;
}
if (FAILED(wic->CreateFormatConverter(&converter)) ||
FAILED(converter->Initialize(scaler, GUID_WICPixelFormat32bppBGRA,
WICBitmapDitherTypeNone, nullptr, 0.0,
WICBitmapPaletteTypeCustom))) {
break;
}
bgra_out.resize(size_t(size) * size * 4);
if (FAILED(converter->CopyPixels(nullptr, size * 4,
UINT(bgra_out.size()), bgra_out.data()))) {
break;
}
ok = true;
} while (false);
if (converter) converter->Release();
if (scaler) scaler->Release();
if (frame) frame->Release();
if (decoder) decoder->Release();
if (stream) stream->Release();
return ok;
}
// Icon-resource DIB image: BITMAPINFOHEADER with DOUBLED height, XOR (BGRA)
// rows bottom-up, then an all-zero AND mask (alpha carries transparency).
std::vector<uint8_t> BuildIconDib(uint32_t size,
const std::vector<uint8_t>& bgra_topdown) {
const uint32_t xor_bytes = size * size * 4;
const uint32_t and_stride = ((size + 31) / 32) * 4;
const uint32_t and_bytes = and_stride * size;
std::vector<uint8_t> out(sizeof(BITMAPINFOHEADER) + xor_bytes + and_bytes, 0);
auto* bih = reinterpret_cast<BITMAPINFOHEADER*>(out.data());
bih->biSize = sizeof(BITMAPINFOHEADER);
bih->biWidth = LONG(size);
bih->biHeight = LONG(size) * 2; // XOR + AND, both bottom-up
bih->biPlanes = 1;
bih->biBitCount = 32;
bih->biCompression = BI_RGB;
bih->biSizeImage = xor_bytes + and_bytes;
uint8_t* xor_dst = out.data() + sizeof(BITMAPINFOHEADER);
for (uint32_t y = 0; y < size; ++y) {
std::memcpy(xor_dst + size_t(y) * size * 4,
bgra_topdown.data() + size_t(size - 1 - y) * size * 4,
size_t(size) * 4);
}
return out;
}
#pragma pack(push, 2)
struct GrpIconDirEntry {
BYTE bWidth;
BYTE bHeight;
BYTE bColorCount;
BYTE bReserved;
WORD wPlanes;
WORD wBitCount;
DWORD dwBytesInRes;
WORD nId;
};
struct GrpIconDir {
WORD idReserved;
WORD idType;
WORD idCount;
};
#pragma pack(pop)
// One-time self-patch: write RT_ICON entries + a MAINICON RT_GROUP_ICON into
// the exe file. The running image is write-locked but RENAMABLE: patch a
// copy, then swap it into place. The renamed original stays locked until
// exit and is deleted by the NEXT (already-patched) run.
void EnsureExeIconResource(const std::vector<uint8_t>& png) {
wchar_t exe_w[MAX_PATH];
if (GetModuleFileNameW(nullptr, exe_w, MAX_PATH) == 0) {
return;
}
const std::filesystem::path exe(exe_w);
const std::filesystem::path pre = exe.wstring() + L".pre-icon";
if (FindResourceW(GetModuleHandleW(nullptr), L"MAINICON",
reinterpret_cast<LPCWSTR>(RT_GROUP_ICON)) != nullptr) {
// Already patched (or a build that ships an icon): just clean up any
// leftover pre-patch original from the run that performed the swap.
DeleteFileW(pre.c_str());
return;
}
// Build the multi-size images. WIC failure falls back to a single raw-PNG
// entry (valid icon image format since Vista).
struct IconImage {
uint32_t size;
std::vector<uint8_t> data;
};
std::vector<IconImage> images;
const HRESULT co = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
{
IWICImagingFactory* wic = nullptr;
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&wic)))) {
for (uint32_t s : {16u, 24u, 32u, 48u, 64u, 256u}) {
std::vector<uint8_t> bgra;
if (WicDecodeScaled(wic, png, s, bgra)) {
images.push_back({s, BuildIconDib(s, bgra)});
}
}
wic->Release();
}
}
if (SUCCEEDED(co)) {
CoUninitialize();
}
if (images.empty()) {
images.push_back({64, png});
}
GrpIconDir dir{0, 1, WORD(images.size())};
std::vector<uint8_t> group(sizeof(GrpIconDir) +
images.size() * sizeof(GrpIconDirEntry));
std::memcpy(group.data(), &dir, sizeof(dir));
for (size_t i = 0; i < images.size(); ++i) {
GrpIconDirEntry e{};
e.bWidth = BYTE(images[i].size >= 256 ? 0 : images[i].size);
e.bHeight = e.bWidth;
e.wPlanes = 1;
e.wBitCount = 32;
e.dwBytesInRes = DWORD(images[i].data.size());
e.nId = WORD(i + 1);
std::memcpy(group.data() + sizeof(GrpIconDir) + i * sizeof(e), &e,
sizeof(e));
}
// Patch a COPY first so a failure changes nothing.
const std::filesystem::path temp = exe.wstring() + L".icon-new";
if (!CopyFileW(exe.c_str(), temp.c_str(), FALSE)) {
REXLOG_INFO("win-icon: exe copy for icon patch failed ({}); skipping",
GetLastError());
return;
}
const WORD lang = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
HANDLE update = BeginUpdateResourceW(temp.c_str(), FALSE);
bool ok = update != nullptr;
for (size_t i = 0; ok && i < images.size(); ++i) {
ok = UpdateResourceW(update, reinterpret_cast<LPCWSTR>(RT_ICON),
MAKEINTRESOURCEW(WORD(i + 1)), lang,
images[i].data.data(), DWORD(images[i].data.size()));
}
if (ok) {
ok = UpdateResourceW(update, reinterpret_cast<LPCWSTR>(RT_GROUP_ICON),
L"MAINICON", lang, group.data(), DWORD(group.size()));
}
if (update != nullptr) {
// Commit (discard on failure).
ok = EndUpdateResourceW(update, !ok) && ok;
}
if (!ok) {
REXLOG_INFO("win-icon: icon resource patch failed ({}); skipping",
GetLastError());
DeleteFileW(temp.c_str());
return;
}
// Swap: rename the (locked but renamable) running exe aside, move the
// patched copy into place. On the second step failing, restore.
if (!MoveFileExW(exe.c_str(), pre.c_str(), MOVEFILE_REPLACE_EXISTING)) {
REXLOG_INFO("win-icon: could not move running exe aside ({}); skipping",
GetLastError());
DeleteFileW(temp.c_str());
return;
}
if (!MoveFileW(temp.c_str(), exe.c_str())) {
MoveFileW(pre.c_str(), exe.c_str());
return;
}
REXLOG_INFO(
"win-icon: Explorer icon patched into {} from the game's own art "
"(one-time; {} removed on next launch)",
exe.filename().string(), pre.filename().string());
}
} // namespace
void ApplyGameIconFromGameData(const std::filesystem::path& game_data_root,
void* native_window_handle) {
std::filesystem::path nxeart = game_data_root / "nxeart";
std::error_code ec;
if (!std::filesystem::exists(nxeart, ec)) {
nxeart = game_data_root / "game" / "nxeart";
}
HWND hwnd = static_cast<HWND>(native_window_handle);
std::thread([nxeart, hwnd] {
const std::vector<uint8_t> png = ExtractFirstPng(nxeart);
if (png.empty()) {
REXLOG_INFO("win-icon: no title icon found in {}", nxeart.string());
return;
}
if (hwnd != nullptr) {
// CreateIconFromResourceEx accepts PNG image data (Vista+). Explicit
// small size for crisp titlebar/taskbar small renditions.
HICON icon_big = CreateIconFromResourceEx(
const_cast<PBYTE>(png.data()), DWORD(png.size()), TRUE, 0x00030000,
0, 0, LR_DEFAULTCOLOR);
HICON icon_small = CreateIconFromResourceEx(
const_cast<PBYTE>(png.data()), DWORD(png.size()), TRUE, 0x00030000,
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON),
LR_DEFAULTCOLOR);
DWORD_PTR unused = 0;
if (icon_big) {
SendMessageTimeoutW(hwnd, WM_SETICON, ICON_BIG,
reinterpret_cast<LPARAM>(icon_big), SMTO_NORMAL,
2000, &unused);
}
if (icon_small) {
SendMessageTimeoutW(hwnd, WM_SETICON, ICON_SMALL,
reinterpret_cast<LPARAM>(icon_small), SMTO_NORMAL,
2000, &unused);
}
// HICONs stay alive for the window's lifetime (never destroyed:
// process-lifetime singletons).
}
EnsureExeIconResource(png);
}).detach();
}
} // namespace skate3
#else // !_WIN32
namespace skate3 {
void ApplyGameIconFromGameData(const std::filesystem::path&, void*) {}
} // namespace skate3
#endif

24
src/skate3_win_icon.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <filesystem>
namespace skate3 {
// Application icon sourced from the USER'S OWN game files at runtime; the
// shipped executable and the repository deliberately contain no EA artwork
// (same clean-distribution rule as every other game asset).
//
// Windows only (no-op elsewhere). Two halves, both fed by the 64x64 title
// tile PNG embedded in game/nxeart (the Xbox dashboard-art package):
// - window/taskbar/Alt-Tab icon: WM_SETICON on the game window, every run;
// - Explorer file icon: a one-time self-patch that writes a MAINICON
// resource group into the exe ON DISK (the running image is rename-and-
// copy swapped since it is write-locked). Idempotent: subsequent runs see
// the resource and skip; overwriting the exe with an update simply
// re-patches on the next run.
//
// Runs on a background thread; failures only log (the icon stays default).
void ApplyGameIconFromGameData(const std::filesystem::path& game_data_root,
void* native_window_handle);
} // namespace skate3