From ab58125f7104202330f8810bd92cfe23343613c8 Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:51:10 -0500 Subject: [PATCH 1/9] bus: add a named shader constant registry shader_bus keeps one float4 lane per registered id, owned by the registrant and readable by anyone. Lanes are created on demand and never removed. Writes land in a pending value that the frame latch copies to the bound value, so a write never splits across a frame. The registry is dumped to the log after level load. --- src/xrEngine/IGame_Level.cpp | 3 + src/xrEngine/device.cpp | 4 + src/xrEngine/shader_bus.cpp | 260 ++++++++++++++++++++++++++ src/xrEngine/shader_bus.h | 43 +++++ src/xrEngine/xrEngine.vcxproj | 2 + src/xrEngine/xrEngine.vcxproj.filters | 6 + 6 files changed, 318 insertions(+) create mode 100644 src/xrEngine/shader_bus.cpp create mode 100644 src/xrEngine/shader_bus.h diff --git a/src/xrEngine/IGame_Level.cpp b/src/xrEngine/IGame_Level.cpp index c953c654cc..15e1c9d852 100644 --- a/src/xrEngine/IGame_Level.cpp +++ b/src/xrEngine/IGame_Level.cpp @@ -11,6 +11,7 @@ #include "CameraManager.h" #include "xr_object.h" #include "feel_sound.h" +#include "shader_bus.h" #include "../xrCore/profiler.h" @@ -140,6 +141,8 @@ bool IGame_Level::Load(u32 dwNum) Device.seqFrame.Add(this); + ShaderBus::dump(); + //SECUROM_MARKER_PERFORMANCE_OFF(10) return true; diff --git a/src/xrEngine/device.cpp b/src/xrEngine/device.cpp index 24009248c2..60a0cc68da 100644 --- a/src/xrEngine/device.cpp +++ b/src/xrEngine/device.cpp @@ -32,6 +32,7 @@ #include "xrSash.h" #include "igame_persistent.h" +#include "shader_bus.h" #pragma comment( lib, "d3dx9.lib" ) @@ -739,6 +740,9 @@ void CRenderDevice::FrameMove() } // Frame move Statistic->EngineTOTAL.Begin(); + + ShaderBus::frame_latch(); + // TODO: HACK to test loading screen. //if(!g_bLoaded) START_PROFILE("Process seqFrame"); diff --git a/src/xrEngine/shader_bus.cpp b/src/xrEngine/shader_bus.cpp new file mode 100644 index 0000000000..47a8d50f5e --- /dev/null +++ b/src/xrEngine/shader_bus.cpp @@ -0,0 +1,260 @@ +#include "stdafx.h" +#pragma hdrstop + +#include "shader_bus.h" + +static xr_vector g_bus_lanes; +static xr_vector g_bus_rejected; +static xrCriticalSection g_bus_lock; + +static bool bus_valid_id(LPCSTR id) +{ + if (!id || id[0] < 'a' || id[0] > 'z') + return false; + + u32 len = xr_strlen(id); + if (len > 32) + return false; + + for (u32 i = 1; i < len; ++i) + { + const char c = id[i]; + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') + continue; + return false; + } + return true; +} + +static int bus_find(LPCSTR id) +{ + shared_str key(id); + for (u32 i = 0; i < g_bus_lanes.size(); ++i) + if (g_bus_lanes[i]->id.equal(key)) + return int(i); + return -1; +} + +static ShaderBus::lane* bus_find_or_add(LPCSTR id) +{ + const int found = bus_find(id); + if (found >= 0) + return g_bus_lanes[found]; + + ShaderBus::lane* l = xr_new(); + l->id = id; + l->index = u16(g_bus_lanes.size()); + g_bus_lanes.push_back(l); + return l; +} + +static u16 bus_next_nonce() +{ + static u32 state = u32(GetTickCount()) | 1u; + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + const u16 n = u16(state); + return n ? n : u16(0xa5a5); +} + +static u32 bus_token(const ShaderBus::lane* l) +{ + return (u32(l->nonce) << 16) | u32(l->index); +} + +ShaderBus::lane* ShaderBus::declare(LPCSTR hlsl_name) +{ + if (!hlsl_name || 0 != strncmp(hlsl_name, "bus_", 4)) + return nullptr; + + LPCSTR id = hlsl_name + 4; + xrCriticalSectionGuard guard(&g_bus_lock); + + if (!bus_valid_id(id)) + { + shared_str key(hlsl_name); + for (u32 i = 0; i < g_bus_rejected.size(); ++i) + if (g_bus_rejected[i].equal(key)) + return nullptr; + + g_bus_rejected.push_back(key); + Msg("! [SHADER-BUS] shader constant %s is not a valid lane name", hlsl_name); + return nullptr; + } + + lane* l = bus_find_or_add(id); + l->hlsl = hlsl_name; + return l; +} + +u32 ShaderBus::try_register(LPCSTR id, LPCSTR owner, LPCSTR description, LPCSTR source) +{ + if (!bus_valid_id(id)) + { + Msg("! [SHADER-BUS] rejected lane id '%s'", id ? id : ""); + return 0; + } + if (!owner || !owner[0]) + { + Msg("! [SHADER-BUS] lane '%s' needs an owner", id); + return 0; + } + if (xr_strlen(owner) > 64) + { + Msg("! [SHADER-BUS] lane '%s' owner is longer than 64 characters", id); + return 0; + } + if (description && xr_strlen(description) > 256) + { + Msg("! [SHADER-BUS] lane '%s' description is longer than 256 characters", id); + return 0; + } + + string256 stored_source; + stored_source[0] = 0; + if (source) + strncpy_s(stored_source, sizeof(stored_source), source, _TRUNCATE); + + xrCriticalSectionGuard guard(&g_bus_lock); + lane* l = bus_find_or_add(id); + + if (l->registered) + { + if (0 == xr_strcmp(l->owner.c_str(), owner)) + return bus_token(l); + + Msg("~ [SHADER-BUS] lane '%s' stays with '%s', '%s' did not take it", id, l->owner.c_str(), owner); + return 0; + } + + l->owner = owner; + l->description = description ? description : ""; + l->source = stored_source; + l->nonce = bus_next_nonce(); + l->registered = true; + + Msg("[SHADER-BUS] lane %s registered by '%s' from '%s'", id, owner, l->source.c_str()); + return bus_token(l); +} + +u32 ShaderBus::register_lane(LPCSTR id, LPCSTR owner, LPCSTR description, LPCSTR source) +{ + const u32 token = try_register(id, owner, description, source); + if (token) + return token; + + string256 held_by, held_from; + held_by[0] = 0; + held_from[0] = 0; + { + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found >= 0 && g_bus_lanes[found]->registered) + { + const lane* l = g_bus_lanes[found]; + xr_strcpy(held_by, l->owner.c_str()); + xr_strcpy(held_from, l->source.c_str()); + } + } + + if (held_by[0]) + Debug.fatal(DEBUG_INFO, + "shader bus lane '%s' already belongs to '%s' registered by '%s', '%s' registered by '%s' cannot take it", + id, held_by, held_from, owner ? owner : "", source ? source : ""); + return 0; +} + +bool ShaderBus::set(u32 token, float x, float y, float z, float w) +{ + const u16 nonce = u16(token >> 16); + const u16 index = u16(token & 0xffff); + if (!nonce) + return false; + + xrCriticalSectionGuard guard(&g_bus_lock); + if (index >= g_bus_lanes.size()) + return false; + + lane* l = g_bus_lanes[index]; + if (!l->registered || l->nonce != nonce) + return false; + + l->pending.set(x, y, z, w); + return true; +} + +bool ShaderBus::get(LPCSTR id, Fvector4& value) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found < 0) + return false; + + value.set(g_bus_lanes[found]->bound); + return true; +} + +bool ShaderBus::has(LPCSTR id) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + return bus_find(id) >= 0; +} + +LPCSTR ShaderBus::describe(LPCSTR id) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found < 0 || !g_bus_lanes[found]->registered) + return nullptr; + return g_bus_lanes[found]->description.c_str(); +} + +LPCSTR ShaderBus::owner_of(LPCSTR id) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found < 0 || !g_bus_lanes[found]->registered) + return nullptr; + return g_bus_lanes[found]->owner.c_str(); +} + +u32 ShaderBus::count() +{ + xrCriticalSectionGuard guard(&g_bus_lock); + return u32(g_bus_lanes.size()); +} + +const ShaderBus::lane* ShaderBus::at(u32 index) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + return (index < g_bus_lanes.size()) ? g_bus_lanes[index] : nullptr; +} + +void ShaderBus::frame_latch() +{ + xrCriticalSectionGuard guard(&g_bus_lock); + for (u32 i = 0; i < g_bus_lanes.size(); ++i) + g_bus_lanes[i]->bound.set(g_bus_lanes[i]->pending); +} + +void ShaderBus::dump() +{ + xrCriticalSectionGuard guard(&g_bus_lock); + Msg("[SHADER-BUS] %d lanes", u32(g_bus_lanes.size())); + for (u32 i = 0; i < g_bus_lanes.size(); ++i) + { + const lane* l = g_bus_lanes[i]; + if (l->registered) + Msg("[SHADER-BUS] bus_%s owner '%s' from '%s' = (%f, %f, %f, %f) %s", + l->id.c_str(), l->owner.c_str(), l->source.c_str(), + l->bound.x, l->bound.y, l->bound.z, l->bound.w, l->description.c_str()); + else + Msg("[SHADER-BUS] bus_%s declared by shaders, not registered", l->id.c_str()); + } +} + +int ShaderBus::version() +{ + return 1; +} diff --git a/src/xrEngine/shader_bus.h b/src/xrEngine/shader_bus.h new file mode 100644 index 0000000000..d97ad2dacf --- /dev/null +++ b/src/xrEngine/shader_bus.h @@ -0,0 +1,43 @@ +#pragma once + +namespace ShaderBus +{ + struct lane + { + shared_str id; + shared_str hlsl; + shared_str owner; + shared_str source; + shared_str description; + Fvector4 pending; + Fvector4 bound; + u16 index; + u16 nonce; + bool registered; + + lane() : index(0), nonce(0), registered(false) + { + pending.set(0.f, 0.f, 0.f, 0.f); + bound.set(0.f, 0.f, 0.f, 0.f); + } + }; + + // lanes are never removed so a returned pointer stays valid for the process lifetime + ENGINE_API lane* declare(LPCSTR hlsl_name); + + ENGINE_API u32 register_lane(LPCSTR id, LPCSTR owner, LPCSTR description, LPCSTR source); + ENGINE_API u32 try_register(LPCSTR id, LPCSTR owner, LPCSTR description, LPCSTR source); + ENGINE_API bool set(u32 token, float x, float y, float z, float w); + ENGINE_API bool get(LPCSTR id, Fvector4& value); + ENGINE_API bool has(LPCSTR id); + ENGINE_API LPCSTR describe(LPCSTR id); + ENGINE_API LPCSTR owner_of(LPCSTR id); + ENGINE_API u32 count(); + ENGINE_API const lane* at(u32 index); + + // copies pending to bound for every lane so a write lands whole on the next frame + ENGINE_API void frame_latch(); + + ENGINE_API void dump(); + ENGINE_API int version(); +} diff --git a/src/xrEngine/xrEngine.vcxproj b/src/xrEngine/xrEngine.vcxproj index ebba928607..5a3ec5ee7f 100644 --- a/src/xrEngine/xrEngine.vcxproj +++ b/src/xrEngine/xrEngine.vcxproj @@ -913,6 +913,7 @@ + @@ -1022,6 +1023,7 @@ + diff --git a/src/xrEngine/xrEngine.vcxproj.filters b/src/xrEngine/xrEngine.vcxproj.filters index 4b104a74ce..6e0ae87c78 100644 --- a/src/xrEngine/xrEngine.vcxproj.filters +++ b/src/xrEngine/xrEngine.vcxproj.filters @@ -435,6 +435,9 @@ Game API\Objects + + Game API\Objects + Interfaces\Discord @@ -545,6 +548,9 @@ Game API\Objects + + Game API\Objects + Game API\Objects From 7a6627afbe1b6bf8ee97bb7f4fba7529f5001a34 Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:51:11 -0500 Subject: [PATCH 2/9] bus: bind shader constants named bus_ to their registry lane SetMapping walks the compiled constant table after the generic setup list and attaches a binder to every constant whose name starts with bus_, so a shader reaches a lane by declaring it. The lane resolves at attach time and one binder serves every shader that declares it. --- .../Blender_Recorder_StandartBinding.cpp | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp index 6eaf600966..a14d48be8a 100644 --- a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp +++ b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp @@ -12,6 +12,7 @@ #include "../../xrEngine/igame_persistent.h" #include "../../xrEngine/environment.h" +#include "../../xrEngine/shader_bus.h" #include "dxRenderDeviceRender.h" @@ -1356,6 +1357,24 @@ static class vignette_control : public R_constant_setup } } vignette_control; +class bus_binder : public R_constant_setup +{ + ShaderBus::lane* lane; + +public: + bus_binder(ShaderBus::lane* l) : lane(l) + { + } + + virtual void setup(R_constant* C) + { + RCache.set_c(C, lane->bound.x, lane->bound.y, lane->bound.z, lane->bound.w); + } +}; + +// one binder per lane so the pass table dedup, which compares handler pointers, still matches +static xr_vector bus_binders; + // Standart constant-binding void CBlender_Compile::SetMapping() { @@ -1528,6 +1547,28 @@ void CBlender_Compile::SetMapping() r_Constant(*cs.first, cs.second); } + for (u32 it = 0; it < ctable.table.size(); it++) + { + R_constant* C = &*ctable.table[it]; + if (C->type != RC_float) + continue; + + LPCSTR cname = C->name.c_str(); + if (!cname || 0 != strncmp(cname, "bus_", 4)) + continue; + + ShaderBus::lane* lane = ShaderBus::declare(cname); + if (!lane) + continue; + + if (bus_binders.size() <= lane->index) + bus_binders.resize(lane->index + 1, nullptr); + if (!bus_binders[lane->index]) + bus_binders[lane->index] = xr_new(lane); + + C->handler = bus_binders[lane->index]; + } + r_Constant("L_glowing", &binder_silencer_glowing); //--DSR-- SilencerOverheat //--DSR-- HeatVision_start From 657c6c097fffa6cce29ffb2f067f83f7ad502be5 Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:51:11 -0500 Subject: [PATCH 3/9] bus: export the shader bus to lua Module shader_bus registers a lane and hands back a token, and only that token can write the lane. Reads, listing and describe are open to any script. register, try_register and list take the calling lua_State through luabind's raw policy, so the registering script is read from the thread that made the call and a duplicate id from a second mod names both scripts in the fatal. --- src/xrGame/shader_bus_script.cpp | 151 ++++++++++++++++++ src/xrGame/shader_bus_script.h | 8 + src/xrGame/xrGame.vcxproj | 5 + src/xrGame/xrGame.vcxproj.filters | 6 + src/xrServerEntities/script_engine_export.cpp | 1 + src/xrServerEntities/script_engine_export.h | 1 + 6 files changed, 172 insertions(+) create mode 100644 src/xrGame/shader_bus_script.cpp create mode 100644 src/xrGame/shader_bus_script.h diff --git a/src/xrGame/shader_bus_script.cpp b/src/xrGame/shader_bus_script.cpp new file mode 100644 index 0000000000..0f994bb293 --- /dev/null +++ b/src/xrGame/shader_bus_script.cpp @@ -0,0 +1,151 @@ +#include "pch_script.h" +#include "shader_bus_script.h" +#include "../xrEngine/shader_bus.h" +#include "../xrEngine/xr_ioconsole.h" +#include "../xrEngine/xr_ioc_cmd.h" + +using namespace luabind; + +static void bus_caller_source(lua_State* L, string_path& dest) +{ + xr_strcpy(dest, "?"); + + if (!L) + return; + + lua_Debug ar; + for (int level = 0; lua_getstack(L, level, &ar); ++level) + { + if (!lua_getinfo(L, "S", &ar)) + return; + if (ar.what && 0 != xr_strcmp(ar.what, "C")) + { + xr_strcpy(dest, ar.short_src); + return; + } + } +} + +static ::luabind::object bus_token_object(lua_State* L, u32 token) +{ + if (token) + return ::luabind::object(L, token); + + ::luabind::object none(L); + lua_pushnil(L); + none.set(); + return none; +} + +static ::luabind::object bus_register(lua_State* L, LPCSTR id, LPCSTR owner, LPCSTR description) +{ + string_path source; + bus_caller_source(L, source); + + return bus_token_object(L, ShaderBus::register_lane(id, owner, description, source)); +} + +static ::luabind::object bus_try_register(lua_State* L, LPCSTR id, LPCSTR owner, LPCSTR description) +{ + string_path source; + bus_caller_source(L, source); + + return bus_token_object(L, ShaderBus::try_register(id, owner, description, source)); +} + +static bool bus_set(u32 token, float x, float y, float z, float w) +{ + return ShaderBus::set(token, x, y, z, w); +} + +static bool bus_get(LPCSTR id, float& x, float& y, float& z, float& w) +{ + Fvector4 v; + const bool found = ShaderBus::get(id, v); + if (!found) + v.set(0.f, 0.f, 0.f, 0.f); + + x = v.x; + y = v.y; + z = v.z; + w = v.w; + return found; +} + +static bool bus_has(LPCSTR id) +{ + return ShaderBus::has(id); +} + +static LPCSTR bus_describe(LPCSTR id) +{ + return ShaderBus::describe(id); +} + +static LPCSTR bus_owner_of(LPCSTR id) +{ + return ShaderBus::owner_of(id); +} + +static int bus_version() +{ + return ShaderBus::version(); +} + +static ::luabind::object bus_list(lua_State* L) +{ + ::luabind::object rows = ::luabind::newtable(L); + + int row_index = 1; + const u32 lanes = ShaderBus::count(); + for (u32 i = 0; i < lanes; ++i) + { + const ShaderBus::lane* l = ShaderBus::at(i); + if (!l || !l->registered) + continue; + + ::luabind::object row = ::luabind::newtable(L); + row["id"] = l->id.c_str(); + row["owner"] = l->owner.c_str(); + row["description"] = l->description.c_str(); + rows[row_index++] = row; + } + + static LPCSTR legacy_lanes[] = { + "shader_param_1", "shader_param_2", "shader_param_3", "shader_param_4", + "shader_param_5", "shader_param_6", "shader_param_7", "shader_param_8", + "s3ds_param_1", "s3ds_param_2", "s3ds_param_3", "s3ds_param_4" + }; + + for (u32 i = 0; i < sizeof(legacy_lanes) / sizeof(legacy_lanes[0]); ++i) + { + IConsole_Command* cc = Console ? Console->GetCommand(legacy_lanes[i]) : nullptr; + if (!cc || !smart_cast(cc)) + continue; + + ::luabind::object row = ::luabind::newtable(L); + row["id"] = legacy_lanes[i]; + row["owner"] = "engine legacy"; + row["description"] = ""; + rows[row_index++] = row; + } + return rows; +} + +#pragma optimize("s",on) +void shader_bus_registrator::script_register(lua_State* L) +{ + module(L, "shader_bus") + [ + def("register", &bus_register, raw<1>()), + def("try_register", &bus_try_register, raw<1>()), + def("set", &bus_set), + def("get", &bus_get, + pure_out_value<2>() + pure_out_value<3>() + pure_out_value<4>() + pure_out_value<5>()), + def("has", &bus_has), + def("describe", &bus_describe), + def("owner_of", &bus_owner_of), + def("list", &bus_list, raw<1>()), + def("version", &bus_version) + ]; +} diff --git a/src/xrGame/shader_bus_script.h b/src/xrGame/shader_bus_script.h new file mode 100644 index 0000000000..68d3ae300f --- /dev/null +++ b/src/xrGame/shader_bus_script.h @@ -0,0 +1,8 @@ +#pragma once + +#include "script_export_space.h" + +struct shader_bus_registrator +{ +DECLARE_SCRIPT_REGISTER_FUNCTION +}; diff --git a/src/xrGame/xrGame.vcxproj b/src/xrGame/xrGame.vcxproj index ccbcc14b2c..15753fddc2 100644 --- a/src/xrGame/xrGame.vcxproj +++ b/src/xrGame/xrGame.vcxproj @@ -1005,6 +1005,7 @@ + @@ -2699,6 +2700,10 @@ pch_script.h $(IntDir)$(ProjectName)_script.pch + + pch_script.h + $(IntDir)$(ProjectName)_script.pch + diff --git a/src/xrGame/xrGame.vcxproj.filters b/src/xrGame/xrGame.vcxproj.filters index 4334b441a2..0c55468429 100644 --- a/src/xrGame/xrGame.vcxproj.filters +++ b/src/xrGame/xrGame.vcxproj.filters @@ -5154,6 +5154,9 @@ AI\AScript\ScriptClasses\Console + + AI\AScript\ScriptClasses\Console + AI\AScript\ScriptClasses\BaseClientClasses @@ -8768,6 +8771,9 @@ AI\AScript\ScriptClasses\Console + + AI\AScript\ScriptClasses\Console + AI\AScript\ScriptClasses\BaseClientClasses diff --git a/src/xrServerEntities/script_engine_export.cpp b/src/xrServerEntities/script_engine_export.cpp index 9af7c94bd3..d74be30f70 100644 --- a/src/xrServerEntities/script_engine_export.cpp +++ b/src/xrServerEntities/script_engine_export.cpp @@ -58,6 +58,7 @@ void export_classes (lua_State *L) CALifeSmartTerrainTask::script_register(L); CClientSpawnManager::script_register(L); console_registrator::script_register(L); + shader_bus_registrator::script_register(L); CCoverPoint::script_register(L); demo_player_info::script_register(L); demo_info::script_register(L); diff --git a/src/xrServerEntities/script_engine_export.h b/src/xrServerEntities/script_engine_export.h index f7dd9a66e0..dadd4d38f2 100644 --- a/src/xrServerEntities/script_engine_export.h +++ b/src/xrServerEntities/script_engine_export.h @@ -68,6 +68,7 @@ # include "key_binding_registrator.h" # include "fs_registrator.h" # include "console_registrator.h" +# include "shader_bus_script.h" # include "physics_shell_scripted.h" # include "physics_joint_scripted.h" # include "physics_element_scripted.h" From ca08a63332641a6ce342d0697d7435199d65d6fe Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:51:11 -0500 Subject: [PATCH 4/9] bus: add bus_list and bus_get and deprecate the shared param lanes bus_list prints every registered lane with its owner and value, the lanes a shader declared but nobody claimed, and the twelve shared console lanes. bus_get prints one lane. The eight shader_param and four s3ds_param commands log once per session on their first write pointing at the bus. --- src/Layers/xrRender/xrRender_console.cpp | 126 ++++++++++++++++++++--- 1 file changed, 113 insertions(+), 13 deletions(-) diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index aa8636e3d3..d7a0d2cc40 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -520,6 +520,7 @@ int opt_dynamic = 2; #ifndef _EDITOR #include "../../xrEngine/xr_ioconsole.h" #include "../../xrEngine/xr_ioc_cmd.h" +#include "../../xrEngine/shader_bus.h" #if defined(USE_DX10) || defined(USE_DX11) #include "../xrRenderDX10/StateManager/dx10SamplerStateCache.h" @@ -1063,6 +1064,102 @@ class CCC_Fog_Reload : public IConsole_Command #endif // DEBUG #endif // (RENDER == R_R3) || (RENDER == R_R4) +static const struct +{ + LPCSTR name; + Fvector4* value; +} legacy_lanes[] = { + {"shader_param_1", &ps_dev_param_1}, + {"shader_param_2", &ps_dev_param_2}, + {"shader_param_3", &ps_dev_param_3}, + {"shader_param_4", &ps_dev_param_4}, + {"shader_param_5", &ps_dev_param_5}, + {"shader_param_6", &ps_dev_param_6}, + {"shader_param_7", &ps_dev_param_7}, + {"shader_param_8", &ps_dev_param_8}, + {"s3ds_param_1", &ps_s3ds_param_1}, + {"s3ds_param_2", &ps_s3ds_param_2}, + {"s3ds_param_3", &ps_s3ds_param_3}, + {"s3ds_param_4", &ps_s3ds_param_4} +}; + +class CCC_BusList : public IConsole_Command +{ +public: + CCC_BusList(LPCSTR N) : IConsole_Command(N) { bEmptyArgsHandled = TRUE; }; + + virtual void Execute(LPCSTR args) + { + const u32 lanes = ShaderBus::count(); + Msg("[SHADER-BUS] %d lanes", lanes); + + for (u32 i = 0; i < lanes; ++i) + { + const ShaderBus::lane* l = ShaderBus::at(i); + if (!l) + continue; + + if (l->registered) + Msg("[SHADER-BUS] bus_%s owner '%s' = (%f, %f, %f, %f) %s", + l->id.c_str(), l->owner.c_str(), + l->bound.x, l->bound.y, l->bound.z, l->bound.w, l->description.c_str()); + else + Msg("[SHADER-BUS] bus_%s declared by shaders, not registered", l->id.c_str()); + } + + for (u32 i = 0; i < sizeof(legacy_lanes) / sizeof(legacy_lanes[0]); ++i) + Msg("[SHADER-BUS] %s legacy = (%f, %f, %f, %f)", legacy_lanes[i].name, + legacy_lanes[i].value->x, legacy_lanes[i].value->y, + legacy_lanes[i].value->z, legacy_lanes[i].value->w); + } +}; + +class CCC_BusGet : public IConsole_Command +{ +public: + CCC_BusGet(LPCSTR N) : IConsole_Command(N) { bEmptyArgsHandled = TRUE; }; + + virtual void Execute(LPCSTR args) + { + if (!args || !args[0]) + { + Msg("~ [SHADER-BUS] usage bus_get "); + return; + } + + Fvector4 v; + if (!ShaderBus::get(args, v)) + { + Msg("~ [SHADER-BUS] no lane named %s", args); + return; + } + Msg("[SHADER-BUS] bus_%s = (%f, %f, %f, %f)", args, v.x, v.y, v.z, v.w); + } + + virtual void Info(TInfo& I) { xr_strcpy(I, "lane id"); } +}; + +class CCC_Vector4Legacy : public CCC_Vector4 +{ + bool warned; + +public: + CCC_Vector4Legacy(LPCSTR N, Fvector4* V, const Fvector4 _min, const Fvector4 _max) : + CCC_Vector4(N, V, _min, _max), warned(false) + { + }; + + virtual void Execute(LPCSTR args) + { + if (!warned && Device.b_is_Ready) + { + warned = true; + Msg("~ [SHADER-BUS] %s is a legacy lane, register a shader_bus lane instead", cName); + } + CCC_Vector4::Execute(args); + } +}; + //----------------------------------------------------------------------- void xrRender_initconsole() { @@ -1319,25 +1416,28 @@ void xrRender_initconsole() //Shader param stuff Fvector4 tw2_min = { -100.f, -100.f, -100.f, -100.f }; Fvector4 tw2_max = { 100.f, 100.f, 100.f, 100.f }; - CMD4(CCC_Vector4, "shader_param_1", &ps_dev_param_1, tw2_min, tw2_max); - CMD4(CCC_Vector4, "shader_param_2", &ps_dev_param_2, tw2_min, tw2_max); - CMD4(CCC_Vector4, "shader_param_3", &ps_dev_param_3, tw2_min, tw2_max); - CMD4(CCC_Vector4, "shader_param_4", &ps_dev_param_4, tw2_min, tw2_max); - CMD4(CCC_Vector4, "shader_param_5", &ps_dev_param_5, tw2_min, tw2_max); - CMD4(CCC_Vector4, "shader_param_6", &ps_dev_param_6, tw2_min, tw2_max); - CMD4(CCC_Vector4, "shader_param_7", &ps_dev_param_7, tw2_min, tw2_max); - CMD4(CCC_Vector4, "shader_param_8", &ps_dev_param_8, tw2_min, tw2_max); - + CMD4(CCC_Vector4Legacy, "shader_param_1", &ps_dev_param_1, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "shader_param_2", &ps_dev_param_2, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "shader_param_3", &ps_dev_param_3, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "shader_param_4", &ps_dev_param_4, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "shader_param_5", &ps_dev_param_5, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "shader_param_6", &ps_dev_param_6, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "shader_param_7", &ps_dev_param_7, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "shader_param_8", &ps_dev_param_8, tw2_min, tw2_max); + + CMD1(CCC_BusList, "bus_list"); + CMD1(CCC_BusGet, "bus_get"); + // Mark Switch CMD4(CCC_Integer, "markswitch_current", &ps_markswitch_current, 0, 32); CMD4(CCC_Integer, "markswitch_count", &ps_markswitch_count, 0, 32); CMD4(CCC_Vector4, "markswitch_color", &ps_markswitch_color, Fvector4().set(0.0, 0.0, 0.0, 0.0), Fvector4().set(1.0, 1.0, 1.0, 1.0)); // Shader 3D Scopes - CMD4(CCC_Vector4, "s3ds_param_1", &ps_s3ds_param_1, tw2_min, tw2_max); - CMD4(CCC_Vector4, "s3ds_param_2", &ps_s3ds_param_2, tw2_min, tw2_max); - CMD4(CCC_Vector4, "s3ds_param_3", &ps_s3ds_param_3, tw2_min, tw2_max); - CMD4(CCC_Vector4, "s3ds_param_4", &ps_s3ds_param_4, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "s3ds_param_1", &ps_s3ds_param_1, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "s3ds_param_2", &ps_s3ds_param_2, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "s3ds_param_3", &ps_s3ds_param_3, tw2_min, tw2_max); + CMD4(CCC_Vector4Legacy, "s3ds_param_4", &ps_s3ds_param_4, tw2_min, tw2_max); CMD4(CCC_Float, "hud_fov_aim_factor", &hud_fov_aim_factor, 0.0f, 1.0f); From 25a77bfebe15751d7169dba40573714374b1edbb Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:51:12 -0500 Subject: [PATCH 5/9] bus: lanes count traffic and carry a debug hold Each lane counts writes, the set calls made through its token, and changes, the value changes the latch sees, so a lane rewritten every frame with the same value shows many writes and no changes. The binder stamps bound_frame with the last frame a shader read the lane. The bus version is 2. list() rows carry state and source, list(true) adds the lanes a shader declared that nobody registered, stats(id) returns the counters and frames, and get_pending(id) reads the value the owner last wrote. describe and owner_of still return nil for an unknown id. bus_force x y z w holds a lane at finite values, the latch feeds the held value to shaders while the owner keeps writing pending, and bus_release hands the lane back on the next latch. This is a console debug path, the lua write path still needs the token. bus_list prints state, counters and the last bound frame. --- .../Blender_Recorder_StandartBinding.cpp | 1 + src/Layers/xrRender/xrRender_console.cpp | 71 ++++++++++++++++++- src/xrEngine/shader_bus.cpp | 66 ++++++++++++++++- src/xrEngine/shader_bus.h | 16 ++++- src/xrGame/shader_bus_script.cpp | 47 ++++++++++-- 5 files changed, 190 insertions(+), 11 deletions(-) diff --git a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp index a14d48be8a..1ce327abaf 100644 --- a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp +++ b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp @@ -1368,6 +1368,7 @@ class bus_binder : public R_constant_setup virtual void setup(R_constant* C) { + lane->bound_frame = Device.dwFrame; RCache.set_c(C, lane->bound.x, lane->bound.y, lane->bound.z, lane->bound.w); } }; diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index d7a0d2cc40..9217ae0994 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -1100,11 +1100,13 @@ class CCC_BusList : public IConsole_Command continue; if (l->registered) - Msg("[SHADER-BUS] bus_%s owner '%s' = (%f, %f, %f, %f) %s", + Msg("[SHADER-BUS] bus_%s owner '%s' = (%f, %f, %f, %f) %s [%s changes %d writes %d bound frame %d]", l->id.c_str(), l->owner.c_str(), - l->bound.x, l->bound.y, l->bound.z, l->bound.w, l->description.c_str()); + l->bound.x, l->bound.y, l->bound.z, l->bound.w, l->description.c_str(), + l->is_forced ? "forced" : "registered", l->changes, l->writes, l->bound_frame); else - Msg("[SHADER-BUS] bus_%s declared by shaders, not registered", l->id.c_str()); + Msg("[SHADER-BUS] bus_%s declared by shaders, not registered [%s changes %d writes %d bound frame %d]", + l->id.c_str(), l->is_forced ? "forced" : "declared", l->changes, l->writes, l->bound_frame); } for (u32 i = 0; i < sizeof(legacy_lanes) / sizeof(legacy_lanes[0]); ++i) @@ -1139,6 +1141,67 @@ class CCC_BusGet : public IConsole_Command virtual void Info(TInfo& I) { xr_strcpy(I, "lane id"); } }; +class CCC_BusForce : public IConsole_Command +{ +public: + CCC_BusForce(LPCSTR N) : IConsole_Command(N) { bEmptyArgsHandled = TRUE; }; + + virtual void Execute(LPCSTR args) + { + string64 id; + id[0] = 0; + + Fvector4 v; + if (!args || 5 != sscanf(args, "%63s %f %f %f %f", id, &v.x, &v.y, &v.z, &v.w)) + { + Msg("~ [SHADER-BUS] usage bus_force x y z w"); + return; + } + + if (!_finite(v.x) || !_finite(v.y) || !_finite(v.z) || !_finite(v.w)) + { + Msg("~ [SHADER-BUS] bus_force needs finite values"); + return; + } + + if (!ShaderBus::force(id, v)) + { + Msg("~ [SHADER-BUS] no lane named %s", id); + return; + } + Msg("[SHADER-BUS] bus_%s held at (%f, %f, %f, %f)", id, v.x, v.y, v.z, v.w); + } + + virtual void Info(TInfo& I) { xr_strcpy(I, "lane id and four floats"); } +}; + +class CCC_BusRelease : public IConsole_Command +{ +public: + CCC_BusRelease(LPCSTR N) : IConsole_Command(N) { bEmptyArgsHandled = TRUE; }; + + virtual void Execute(LPCSTR args) + { + string64 id; + id[0] = 0; + + if (!args || 1 != sscanf(args, "%63s", id)) + { + Msg("~ [SHADER-BUS] usage bus_release "); + return; + } + + if (!ShaderBus::release(id)) + { + Msg("~ [SHADER-BUS] no lane named %s", id); + return; + } + Msg("[SHADER-BUS] bus_%s released", id); + } + + virtual void Info(TInfo& I) { xr_strcpy(I, "lane id"); } +}; + class CCC_Vector4Legacy : public CCC_Vector4 { bool warned; @@ -1427,6 +1490,8 @@ void xrRender_initconsole() CMD1(CCC_BusList, "bus_list"); CMD1(CCC_BusGet, "bus_get"); + CMD1(CCC_BusForce, "bus_force"); + CMD1(CCC_BusRelease, "bus_release"); // Mark Switch CMD4(CCC_Integer, "markswitch_current", &ps_markswitch_current, 0, 32); diff --git a/src/xrEngine/shader_bus.cpp b/src/xrEngine/shader_bus.cpp index 47a8d50f5e..c91aa463e0 100644 --- a/src/xrEngine/shader_bus.cpp +++ b/src/xrEngine/shader_bus.cpp @@ -181,6 +181,7 @@ bool ShaderBus::set(u32 token, float x, float y, float z, float w) return false; l->pending.set(x, y, z, w); + ++l->writes; return true; } @@ -219,6 +220,55 @@ LPCSTR ShaderBus::owner_of(LPCSTR id) return g_bus_lanes[found]->owner.c_str(); } +bool ShaderBus::stats(LPCSTR id, u32& changes, u32& last_change, u32& bound_frame, u32& writes) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found < 0) + return false; + + const lane* l = g_bus_lanes[found]; + changes = l->changes; + last_change = l->last_change_frame; + bound_frame = l->bound_frame; + writes = l->writes; + return true; +} + +bool ShaderBus::get_pending(LPCSTR id, Fvector4& value) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found < 0) + return false; + + value.set(g_bus_lanes[found]->pending); + return true; +} + +bool ShaderBus::force(LPCSTR id, const Fvector4& value) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found < 0) + return false; + + g_bus_lanes[found]->forced.set(value); + g_bus_lanes[found]->is_forced = true; + return true; +} + +bool ShaderBus::release(LPCSTR id) +{ + xrCriticalSectionGuard guard(&g_bus_lock); + const int found = bus_find(id); + if (found < 0) + return false; + + g_bus_lanes[found]->is_forced = false; + return true; +} + u32 ShaderBus::count() { xrCriticalSectionGuard guard(&g_bus_lock); @@ -235,7 +285,19 @@ void ShaderBus::frame_latch() { xrCriticalSectionGuard guard(&g_bus_lock); for (u32 i = 0; i < g_bus_lanes.size(); ++i) - g_bus_lanes[i]->bound.set(g_bus_lanes[i]->pending); + { + lane* l = g_bus_lanes[i]; + const Fvector4& src = l->is_forced ? l->forced : l->pending; + + if (src.x != l->bound.x || src.y != l->bound.y || + src.z != l->bound.z || src.w != l->bound.w) + { + ++l->changes; + l->last_change_frame = Device.dwFrame; + } + + l->bound.set(src); + } } void ShaderBus::dump() @@ -256,5 +318,5 @@ void ShaderBus::dump() int ShaderBus::version() { - return 1; + return 2; } diff --git a/src/xrEngine/shader_bus.h b/src/xrEngine/shader_bus.h index d97ad2dacf..9bded1f093 100644 --- a/src/xrEngine/shader_bus.h +++ b/src/xrEngine/shader_bus.h @@ -11,14 +11,22 @@ namespace ShaderBus shared_str description; Fvector4 pending; Fvector4 bound; + Fvector4 forced; + u32 changes; + u32 writes; + u32 last_change_frame; + u32 bound_frame; u16 index; u16 nonce; bool registered; + bool is_forced; - lane() : index(0), nonce(0), registered(false) + lane() : changes(0), writes(0), last_change_frame(0), bound_frame(0), index(0), nonce(0), registered(false), + is_forced(false) { pending.set(0.f, 0.f, 0.f, 0.f); bound.set(0.f, 0.f, 0.f, 0.f); + forced.set(0.f, 0.f, 0.f, 0.f); } }; @@ -32,10 +40,14 @@ namespace ShaderBus ENGINE_API bool has(LPCSTR id); ENGINE_API LPCSTR describe(LPCSTR id); ENGINE_API LPCSTR owner_of(LPCSTR id); + ENGINE_API bool stats(LPCSTR id, u32& changes, u32& last_change, u32& bound_frame, u32& writes); + ENGINE_API bool get_pending(LPCSTR id, Fvector4& value); + ENGINE_API bool force(LPCSTR id, const Fvector4& value); + ENGINE_API bool release(LPCSTR id); ENGINE_API u32 count(); ENGINE_API const lane* at(u32 index); - // copies pending to bound for every lane so a write lands whole on the next frame + // copies the forced or pending value to bound and counts a change when it differs ENGINE_API void frame_latch(); ENGINE_API void dump(); diff --git a/src/xrGame/shader_bus_script.cpp b/src/xrGame/shader_bus_script.cpp index 0f994bb293..5909e64797 100644 --- a/src/xrGame/shader_bus_script.cpp +++ b/src/xrGame/shader_bus_script.cpp @@ -87,12 +87,35 @@ static LPCSTR bus_owner_of(LPCSTR id) return ShaderBus::owner_of(id); } +static bool bus_stats(LPCSTR id, u32& changes, u32& last_change, u32& bound_frame, u32& writes) +{ + changes = 0; + last_change = 0; + bound_frame = 0; + writes = 0; + return ShaderBus::stats(id, changes, last_change, bound_frame, writes); +} + +static bool bus_get_pending(LPCSTR id, float& x, float& y, float& z, float& w) +{ + Fvector4 v; + const bool found = ShaderBus::get_pending(id, v); + if (!found) + v.set(0.f, 0.f, 0.f, 0.f); + + x = v.x; + y = v.y; + z = v.z; + w = v.w; + return found; +} + static int bus_version() { return ShaderBus::version(); } -static ::luabind::object bus_list(lua_State* L) +static ::luabind::object bus_list(lua_State* L, bool include_declared) { ::luabind::object rows = ::luabind::newtable(L); @@ -101,13 +124,17 @@ static ::luabind::object bus_list(lua_State* L) for (u32 i = 0; i < lanes; ++i) { const ShaderBus::lane* l = ShaderBus::at(i); - if (!l || !l->registered) + if (!l) + continue; + if (!l->registered && !include_declared) continue; ::luabind::object row = ::luabind::newtable(L); row["id"] = l->id.c_str(); - row["owner"] = l->owner.c_str(); - row["description"] = l->description.c_str(); + row["owner"] = l->registered ? l->owner.c_str() : ""; + row["description"] = l->registered ? l->description.c_str() : ""; + row["state"] = l->registered ? "registered" : "declared"; + row["source"] = l->registered ? l->source.c_str() : ""; rows[row_index++] = row; } @@ -127,11 +154,18 @@ static ::luabind::object bus_list(lua_State* L) row["id"] = legacy_lanes[i]; row["owner"] = "engine legacy"; row["description"] = ""; + row["state"] = "legacy"; + row["source"] = ""; rows[row_index++] = row; } return rows; } +static ::luabind::object bus_list_registered(lua_State* L) +{ + return bus_list(L, false); +} + #pragma optimize("s",on) void shader_bus_registrator::script_register(lua_State* L) { @@ -145,6 +179,11 @@ void shader_bus_registrator::script_register(lua_State* L) def("has", &bus_has), def("describe", &bus_describe), def("owner_of", &bus_owner_of), + def("stats", &bus_stats, + pure_out_value<2>() + pure_out_value<3>() + pure_out_value<4>() + pure_out_value<5>()), + def("get_pending", &bus_get_pending, + pure_out_value<2>() + pure_out_value<3>() + pure_out_value<4>() + pure_out_value<5>()), + def("list", &bus_list_registered, raw<1>()), def("list", &bus_list, raw<1>()), def("version", &bus_version) ]; From e3e921f1f1364a1b5b9e23f185efb60460066ac4 Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:11:01 -0500 Subject: [PATCH 6/9] bus: legacy lanes name the script that writes them The lua console export records the calling script and line for the duration of the execute in a thread local console slot, walking past C frames and the _g.script wrapper so the mod that set the lane is named, and a nested execute keeps the outer caller. A valid write to one of the twelve legacy vector commands records that writer, the config file name before the device is up and console when no script is on the stack. The deprecation nudge fires once per command and writer, and the legacy rows of shader_bus.list carry the last writer. --- src/Layers/xrRender/xrRender_console.cpp | 17 +++--- src/xrEngine/XR_IOConsole.cpp | 22 ++++++++ src/xrEngine/XR_IOConsole.h | 9 ++++ src/xrEngine/shader_bus.cpp | 66 +++++++++++++++++++++++ src/xrEngine/shader_bus.h | 3 ++ src/xrEngine/xr_ioc_cmd.h | 34 ++++++------ src/xrGame/console_registrator_script.cpp | 31 ++++++++++- src/xrGame/shader_bus_script.cpp | 4 ++ 8 files changed, 163 insertions(+), 23 deletions(-) diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp index 9217ae0994..cd92327130 100644 --- a/src/Layers/xrRender/xrRender_console.cpp +++ b/src/Layers/xrRender/xrRender_console.cpp @@ -1204,22 +1204,25 @@ class CCC_BusRelease : public IConsole_Command class CCC_Vector4Legacy : public CCC_Vector4 { - bool warned; - public: CCC_Vector4Legacy(LPCSTR N, Fvector4* V, const Fvector4 _min, const Fvector4 _max) : - CCC_Vector4(N, V, _min, _max), warned(false) + CCC_Vector4(N, V, _min, _max) { }; virtual void Execute(LPCSTR args) { - if (!warned && Device.b_is_Ready) + Fvector4 v; + if (!parse(args, v)) { - warned = true; - Msg("~ [SHADER-BUS] %s is a legacy lane, register a shader_bus lane instead", cName); + InvalidSyntax(); + return; } - CCC_Vector4::Execute(args); + value->set(v); + + LPCSTR caller = Console->ScriptCaller(); + LPCSTR writer = caller[0] ? caller : (Device.b_is_Ready ? "console" : Console->ConfigFile); + ShaderBus::note_legacy_write(cName, writer); } }; diff --git a/src/xrEngine/XR_IOConsole.cpp b/src/xrEngine/XR_IOConsole.cpp index 59fd0132fa..667aba182c 100644 --- a/src/xrEngine/XR_IOConsole.cpp +++ b/src/xrEngine/XR_IOConsole.cpp @@ -39,6 +39,8 @@ static u32 const tips_scroll_pos_color = color_rgba(70, 70, 70, 240); ENGINE_API CConsole* Console = NULL; +static thread_local string256 s_script_caller; + static void DumpConsoleVariablesOnCrash() { if (Console) @@ -739,6 +741,26 @@ void CConsole::Execute(LPCSTR cmd) ExecuteCommand(cmd, false); } +LPCSTR CConsole::ScriptCaller() const +{ + return s_script_caller; +} + +CConsole::ScriptCallerScope::ScriptCallerScope(LPCSTR src) +{ + xr_strcpy(prev, s_script_caller); + + if (src && src[0]) + strncpy_s(s_script_caller, sizeof(s_script_caller), src, _TRUNCATE); + else + s_script_caller[0] = 0; +} + +CConsole::ScriptCallerScope::~ScriptCallerScope() +{ + xr_strcpy(s_script_caller, prev); +} + void CConsole::ExecuteScript(LPCSTR str) { u32 str_size = xr_strlen(str); diff --git a/src/xrEngine/XR_IOConsole.h b/src/xrEngine/XR_IOConsole.h index b4fafd9cf5..d0523253ef 100644 --- a/src/xrEngine/XR_IOConsole.h +++ b/src/xrEngine/XR_IOConsole.h @@ -155,6 +155,15 @@ class ENGINE_API CConsole : void ExecuteCommand(LPCSTR cmd, bool record_cmd = true); void SelectCommand(); + LPCSTR ScriptCaller() const; + + struct ScriptCallerScope + { + string256 prev; + ScriptCallerScope(LPCSTR src); + ~ScriptCallerScope(); + }; + bool GetBool(LPCSTR cmd) const; float GetFloat(LPCSTR cmd, float& min, float& max) const; int GetInteger(LPCSTR cmd, int& min, int& max) const; diff --git a/src/xrEngine/shader_bus.cpp b/src/xrEngine/shader_bus.cpp index c91aa463e0..01af51ef91 100644 --- a/src/xrEngine/shader_bus.cpp +++ b/src/xrEngine/shader_bus.cpp @@ -3,8 +3,16 @@ #include "shader_bus.h" +struct bus_legacy_row +{ + shared_str command; + string256 writer; +}; + static xr_vector g_bus_lanes; static xr_vector g_bus_rejected; +static xr_vector g_bus_legacy; +static xr_vector g_bus_legacy_logged; static xrCriticalSection g_bus_lock; static bool bus_valid_id(LPCSTR id) @@ -300,6 +308,64 @@ void ShaderBus::frame_latch() } } +void ShaderBus::note_legacy_write(LPCSTR command, LPCSTR writer) +{ + if (!command || !command[0]) + return; + if (!writer) + writer = ""; + + xrCriticalSectionGuard guard(&g_bus_lock); + + int found = -1; + for (u32 i = 0; i < g_bus_legacy.size() && found < 0; ++i) + if (0 == xr_strcmp(g_bus_legacy[i].command.c_str(), command)) + found = int(i); + + if (found >= 0 && 0 == xr_strcmp(g_bus_legacy[found].writer, writer)) + return; + + if (found < 0) + { + bus_legacy_row row; + row.command = command; + row.writer[0] = 0; + g_bus_legacy.push_back(row); + found = int(g_bus_legacy.size()) - 1; + } + strncpy_s(g_bus_legacy[found].writer, sizeof(g_bus_legacy[found].writer), writer, _TRUNCATE); + + if (!Device.b_is_Ready) + return; + + string512 pair; + xr_sprintf(pair, "%s %s", command, writer); + + shared_str pair_key(pair); + for (u32 i = 0; i < g_bus_legacy_logged.size(); ++i) + if (g_bus_legacy_logged[i].equal(pair_key)) + return; + + g_bus_legacy_logged.push_back(pair_key); + Msg("~ [SHADER-BUS] %s written from %s, register a shader_bus lane instead", command, writer); +} + +bool ShaderBus::legacy_writer(LPCSTR command, string256& out) +{ + out[0] = 0; + if (!command || !command[0]) + return false; + + xrCriticalSectionGuard guard(&g_bus_lock); + for (u32 i = 0; i < g_bus_legacy.size(); ++i) + if (0 == xr_strcmp(g_bus_legacy[i].command.c_str(), command)) + { + xr_strcpy(out, g_bus_legacy[i].writer); + return true; + } + return false; +} + void ShaderBus::dump() { xrCriticalSectionGuard guard(&g_bus_lock); diff --git a/src/xrEngine/shader_bus.h b/src/xrEngine/shader_bus.h index 9bded1f093..dae860f8ae 100644 --- a/src/xrEngine/shader_bus.h +++ b/src/xrEngine/shader_bus.h @@ -50,6 +50,9 @@ namespace ShaderBus // copies the forced or pending value to bound and counts a change when it differs ENGINE_API void frame_latch(); + ENGINE_API void note_legacy_write(LPCSTR command, LPCSTR writer); + ENGINE_API bool legacy_writer(LPCSTR command, string256& out); + ENGINE_API void dump(); ENGINE_API int version(); } diff --git a/src/xrEngine/xr_ioc_cmd.h b/src/xrEngine/xr_ioc_cmd.h index 897818cf3a..77b8c1baba 100644 --- a/src/xrEngine/xr_ioc_cmd.h +++ b/src/xrEngine/xr_ioc_cmd.h @@ -375,6 +375,24 @@ class CCC_Vector4 : public IConsole_Command protected: Fvector4* value; Fvector4 min, max; + + bool parse(LPCSTR args, Fvector4& v) + { + if (4 != sscanf(args, "%f,%f,%f,%f", &v.x, &v.y, &v.z, &v.w)) + { + if (4 != sscanf(args, "(%f,%f,%f,%f)", &v.x, &v.y, &v.z, &v.w)) + return false; + } + + if (v.x < min.x || v.y < min.y || v.z < min.z || v.w < min.w) + return false; + + if (v.x > max.x || v.y > max.y || v.z > max.z || v.w > max.w) + return false; + + return true; + } + public : CCC_Vector4(LPCSTR N, Fvector4* V, const Fvector4 _min, const Fvector4 _max) : @@ -390,21 +408,7 @@ class CCC_Vector4 : public IConsole_Command virtual void Execute(LPCSTR args) { Fvector4 v; - if (4 != sscanf(args, "%f,%f,%f,%f", &v.x, &v.y, &v.z, &v.w)) - { - if (4 != sscanf(args, "(%f,%f,%f,%f)", &v.x, &v.y, &v.z, &v.w)) - { - InvalidSyntax(); - return; - } - } - - if (v.x < min.x || v.y < min.y || v.z < min.z || v.w < min.w) - { - InvalidSyntax(); - return; - } - if (v.x > max.x || v.y > max.y || v.z > max.z || v.w > max.w) + if (!parse(args, v)) { InvalidSyntax(); return; diff --git a/src/xrGame/console_registrator_script.cpp b/src/xrGame/console_registrator_script.cpp index 5a78d3c45b..81e97aaa16 100644 --- a/src/xrGame/console_registrator_script.cpp +++ b/src/xrGame/console_registrator_script.cpp @@ -37,6 +37,35 @@ void execute_console_command_deferred(CConsole* c, LPCSTR string_to_execute) Engine.Event.Defer("KERNEL:console", size_t(xr_strdup(string_to_execute))); } +static void console_execute(lua_State* L, CConsole* c, LPCSTR cmd) +{ + string256 src = ""; + lua_Debug ar; + for (int level = 1; level <= 8 && lua_getstack(L, level, &ar); ++level) + { + if (!lua_getinfo(L, "Sl", &ar)) + break; + + if (1 == level) + xr_sprintf(src, "%s:%d", ar.short_src, ar.currentline); + + if ('C' == ar.what[0]) + continue; + + const u32 tail = sizeof("_g.script") - 1; + const u32 len = xr_strlen(ar.short_src); + if (len >= tail && 0 == xr_strcmp(ar.short_src + len - tail, "_g.script") && + (len == tail || '\\' == ar.short_src[len - tail - 1] || '/' == ar.short_src[len - tail - 1])) + continue; + + xr_sprintf(src, "%s:%d", ar.short_src, ar.currentline); + break; + } + + CConsole::ScriptCallerScope scope(src); + c->Execute(cmd); +} + ::luabind::object get_console_bounds(CConsole* c, LPCSTR cmd) { IConsole_Command* command = c->GetCommand(cmd); @@ -90,7 +119,7 @@ void console_registrator::script_register(lua_State* L) def("get_console", &console), class_("CConsole") - .def("execute", &CConsole::Execute) + .def("execute", &console_execute, raw<1>()) .def("execute_script", &CConsole::ExecuteScript) .def("show", &CConsole::Show) .def("hide", &CConsole::Hide) diff --git a/src/xrGame/shader_bus_script.cpp b/src/xrGame/shader_bus_script.cpp index 5909e64797..7d6fba02b6 100644 --- a/src/xrGame/shader_bus_script.cpp +++ b/src/xrGame/shader_bus_script.cpp @@ -150,11 +150,15 @@ static ::luabind::object bus_list(lua_State* L, bool include_declared) if (!cc || !smart_cast(cc)) continue; + string256 writer; + ShaderBus::legacy_writer(legacy_lanes[i], writer); + ::luabind::object row = ::luabind::newtable(L); row["id"] = legacy_lanes[i]; row["owner"] = "engine legacy"; row["description"] = ""; row["state"] = "legacy"; + row["writer"] = (LPCSTR)writer; row["source"] = ""; rows[row_index++] = row; } From 870275b4b00953f845f4aa2b1978b796ca1e804d Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:23:38 -0500 Subject: [PATCH 7/9] bus: the lane dump prints only with -dbg The level load count line and the per lane value lines now need the -dbg key. A lane declared by a shader and registered by nobody still warns in every log. --- src/xrEngine/shader_bus.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/xrEngine/shader_bus.cpp b/src/xrEngine/shader_bus.cpp index 01af51ef91..dbcbfccf36 100644 --- a/src/xrEngine/shader_bus.cpp +++ b/src/xrEngine/shader_bus.cpp @@ -369,16 +369,20 @@ bool ShaderBus::legacy_writer(LPCSTR command, string256& out) void ShaderBus::dump() { xrCriticalSectionGuard guard(&g_bus_lock); - Msg("[SHADER-BUS] %d lanes", u32(g_bus_lanes.size())); + const bool verbose = 0 != strstr(Core.Params, "-dbg"); + + if (verbose) + Msg("[SHADER-BUS] %d lanes", u32(g_bus_lanes.size())); + for (u32 i = 0; i < g_bus_lanes.size(); ++i) { const lane* l = g_bus_lanes[i]; - if (l->registered) + if (!l->registered) + Msg("~ [SHADER-BUS] bus_%s is declared by a shader and registered by nobody", l->id.c_str()); + else if (verbose) Msg("[SHADER-BUS] bus_%s owner '%s' from '%s' = (%f, %f, %f, %f) %s", l->id.c_str(), l->owner.c_str(), l->source.c_str(), l->bound.x, l->bound.y, l->bound.z, l->bound.w, l->description.c_str()); - else - Msg("[SHADER-BUS] bus_%s declared by shaders, not registered", l->id.c_str()); } } From 464b450897f8fa48132f9f9aff8d44d0de1f236d Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:29:55 -0500 Subject: [PATCH 8/9] bus: document the shader bus SHADER_BUS.md at the repo root next to DXML.md, a patch list entry in the README, and the lua module and console commands in lua_help_ex. --- README.md | 4 + SHADER_BUS.md | 251 ++++++++++++++++++++++++++++ gamedata/scripts/lua_help_ex.script | 20 +++ 3 files changed, 275 insertions(+) create mode 100644 SHADER_BUS.md diff --git a/README.md b/README.md index ea55c9b60e..a5bcaff8bd 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,10 @@ The original engine is used in S.T.A.L.K.E.R. Call of Pripyat game released by G * Allows to modify contents of loaded xml files before processing by engine by utilizing Lua scripts * For more information see DXML.md guide. +* Shader bus + * Named, owned float4 shader constants that replace the shared `shader_param_N` slots. A script registers a lane by name and writes it through a token, any shader reads it by declaring `bus_`, and the twelve legacy commands keep working while the log names whoever still writes them. + * For more information see SHADER_BUS.md guide, the Lua module and console commands are listed in `lua_help_ex.script`. + * Possibility to unlocalize Lua variables in scripts before loading, making them global to the script namespace * For unlocalizing a variable in the script, please refer to documentation in test file in `gamedata/configs/unlocalizers` folder diff --git a/SHADER_BUS.md b/SHADER_BUS.md new file mode 100644 index 0000000000..e3bfed11df --- /dev/null +++ b/SHADER_BUS.md @@ -0,0 +1,251 @@ +# Shader Bus + +--- + +## Intro + +The shader bus is a named, owned replacement for the eight anonymous `shader_param_1..8` and +`s3ds_param_1..4` float4 constants. Any script can register a lane by name, any script can read +any lane, and only the script that registered a lane can write it. The engine needs no advance +list of names, so a mod can claim a lane without anything being patched into the exe for it. + +Requires a modded exe carrying the bus. `shader_bus.version()` returns `2`. A script that wants +to detect the feature should test `shader_bus ~= nil` first, since an older exe has no such +module at all. + +--- + +## The shader side + +Declare a `float4` uniform whose name starts with `bus_`. + +```hlsl +uniform float4 bus_myeffect; +``` + +That is all. When the pass compiles, the renderer sees the name in the reflected constant table +and attaches a binder that writes the lane's current bound value on every constant table switch. +The part of the name after `bus_` is the lane id, so `bus_myeffect` is the lane `myeffect`. + +Declaring costs nothing extra to compile and nothing to run if the constant is never read. A lane +nobody has registered reads `0, 0, 0, 0`, so an unclaimed lane behaves like an unmodded install as +long as your shader treats an all-zero value as off. + +--- + +## The script side + +```lua +local token = nil + +function on_game_start() + if shader_bus then + token = shader_bus.register("myeffect", "My Mod Name", "what this lane carries") + end +end + +function actor_on_update() + if token then + shader_bus.set(token, x, y, z, w) + end +end +``` + +Register once, in `on_game_start`. That runs before the level finishes loading, so the lane is +already in the registry by the time the engine's level-load log prints and by the time any other +script's `shader_bus.list()` call would see it. + +A refused registration hands back `nil`, never a number, so `if token then` is the whole check. +Do not test the token against `0`, which is truthy in Lua. + +### API + +| call | returns | +|---|---| +| `shader_bus.register(id, owner, description)` | a token, or a hard fatal, naming both owners and both script paths, if a different owner already holds the id | +| `shader_bus.try_register(id, owner, description)` | a token, or `nil` if a different owner already holds the id | +| `shader_bus.set(token, x, y, z, w)` | `true` if the token is valid | +| `shader_bus.get(id)` | `ok, x, y, z, w`, the bound value, one frame behind the latest `set` | +| `shader_bus.get_pending(id)` | `ok, x, y, z, w`, the value the owner wrote that has not been latched into the bound value yet | +| `shader_bus.has(id)` | bool | +| `shader_bus.describe(id)` | the description string, or `nil` for an id no registered lane owns | +| `shader_bus.owner_of(id)` | the owner string, or `nil` for an id no registered lane owns | +| `shader_bus.stats(id)` | `ok, changes, last_change_frame, bound_frame, writes` | +| `shader_bus.list()` | rows for the registered lanes, then the twelve legacy lanes | +| `shader_bus.list(true)` | the same, plus a row for every lane a shader declared that nobody registered | +| `shader_bus.version()` | `2` | + +Each `list` row carries `id`, `owner`, `description`, `state` (`registered`, `declared` or +`legacy`), `source` (the script path that called `register`, empty for a declared or legacy row) +and, on a legacy row only, `writer`. Registered rows come first (declared rows too, with the +`true` argument), then the twelve legacy rows, and a legacy row only appears if that console +command still exists on the running exe. + +`writes` in `stats` counts every `set` call the owner made through its token. `changes` counts the +frames on which the published value actually moved, so a lane rewritten every frame with an +unchanging value shows many writes and no changes. `last_change_frame` is the last frame a change +landed, `bound_frame` is the last frame a shader pass actually read the lane, so a `bound_frame` +far behind the current frame means the lane is bound to a shader nothing is drawing. `stats` +answers for a declared-but-unregistered lane too; it comes back `false` with all zeros only when +no lane by that id exists at all. + +Reads and listing are open to every script. Only the token returned by `register` or +`try_register` can write. + +### Console + +- `bus_list` prints every lane with its owner, description, current bound value, state and its + change and write counters, then the twelve legacy lanes and their raw values. +- `bus_get ` prints one lane. +- `bus_force x y z w` pins a lane to a value the engine publishes every frame until + `bus_release`. All four components must be finite or the hold is refused. It does not touch or + reject what the owner writes: `get_pending` still reads the owner's value while the hold is + active, and the owner's value is published again on the very next frame after `bus_release`. It + also works on a declared lane with no owner, which is how you can probe a shader before its mod + exists. +- `bus_release ` lifts a hold, so the latch goes back to publishing the owner's pending value. + +The hold is a debug path for testing a shader against arbitrary values. The Lua write path is +unaffected while a hold is active, and nothing about `bus_force` should be relied on by shipped +mod behavior. + +--- + +## Semantics + +One lane belongs to at most one owner for the life of the process. Once `register` succeeds for +an id, only the token it returned can call `set` on that lane; a different script asking for the +same id gets a fatal naming both owners and both script paths, unless it used `try_register`, in +which case it gets `nil` and a log line instead. + +`set` stores a pending value. The engine copies every lane's pending value (or its forced value, +if held) into its bound value once per frame, before the frame callbacks run, so a four-float +write is never half visible to a shader mid-write. This latch runs every frame the device pumps, +so it is live in the main menu and while loading, not only during gameplay. It is also why `get` +trails `set` by one frame and why `get_pending` and `get` can legitimately disagree. + +Lanes are never removed once declared or registered, so a token stays valid for the rest of the +process, and a lane you query early keeps existing even if nobody ever claims it. + +The twelve legacy commands, `shader_param_1..8` and `s3ds_param_1..4`, still work exactly as +before. The first write to any of them in a session logs a nudge naming the writer, once per +command and writer, so a lane four mods still write prints four lines rather than one. `list()` +carries the last writer of a legacy lane as `writer`: a Lua write reads back as +`