Skip to content

Add per-element, per-model and per-type stream distance functions - #5149

Open
QueryOfficial wants to merge 2 commits into
multitheftauto:masterfrom
QueryOfficial:feature/custom-stream-distance
Open

Add per-element, per-model and per-type stream distance functions#5149
QueryOfficial wants to merge 2 commits into
multitheftauto:masterfrom
QueryOfficial:feature/custom-stream-distance

Conversation

@QueryOfficial

Copy link
Copy Markdown
Contributor

Summary

Adds client-side Lua functions to override the streaming radius per element, per model and per element type, instead of every streamed element being tied to its streamer's fixed radius (markers 600, objects 500, low-LOD objects 1700, pickups 100, peds/players 250, vehicles 250, lights 600).

setElementStreamDistance(element, distance)       -- nil/omitted resets to the inherited value
getElementStreamDistance(element)                 -- effective distance actually used by the streamer
setElementTypeStreamDistance(elementType, distance)
getElementTypeStreamDistance(elementType)         -- override, or the streamer default when unset
setElementModelStreamDistance(modelId, distance)
getElementModelStreamDistance(modelId)            -- false when no override is set

OOP: Element:setStreamDistance(), Element:getStreamDistance(), element.streamDistance.

The effective range is resolved as element > model > element type > streamer default. Accepted range is 1–3000; supported element types are object, vehicle, ped, player, pickup, marker. Everything is client-only — no RPC, bitstream or server changes.

-- small props stop costing GPU time at long range
setElementModelStreamDistance(1337, 60)

-- a large custom vehicle streams in well before it can pop in
setElementStreamDistance(myVehicle, 600)

Implementation notes:

  • CClientStreamElement caches the resolved distance plus its squared / inverse-squared forms, because CClientStreamer::Restream reads them for every active element every frame. The stream-out check now uses a per-element threshold, so the existing +50 hysteresis applies to custom ranges too.
  • Active elements are sorted by fraction of their own range rather than by raw squared distance, so elements with different ranges compete fairly for the pool budget. m_fExpDistance keeps its world-unit meaning because CNametags reads it directly. The swap hysteresis was moved into that same ratio space and is derived from the streamer default, which reproduces the previous behaviour exactly for elements without a custom distance.
  • Each streamer only keeps the camera's 3×3 sector window active, so an element asking for more than the streamer default would never reach Restream. Rather than widening that window for everyone (cost scales with map density), only elements that ask for a longer range are pinned into the active list. With the feature unused, behaviour is unchanged.
  • CClientModelCacheManager had PED_STREAM_IN_DISTANCE / VEHICLE_STREAM_IN_DISTANCE hardcoded to 250. It now follows the streamers' actual largest range, otherwise raising ped/vehicle distance would skip model pre-caching and reintroduce blocking loads. The duplicated +50 constant is now CClientStreamer::STREAM_OUT_EXTRA_DISTANCE.
  • Restream's main loop now advances its iterator before streaming, since the stream in/out events let scripts remove the current element from the active list.

Known behaviour worth calling out: setElementTypeStreamDistance("object", …) also applies to low-LOD objects, which otherwise default to 1700.

Motivation

Fixes #5132.

Every streamed element currently shares one fixed radius per streamer, which is wrong in both directions. Large custom vehicle and object models appear abruptly in front of the camera (wheel/suspension settling artifacts), while small decorative objects are still drawn at 500 units and waste GPU time on lower-end machines. Doing this in Lua means a per-frame distance loop over every element, which is expensive at high element counts and is not frame-accurate. Exposing the streamer's radius at core level fixes both cases with no scripting overhead and no network round trip.

Test plan

Built and tested on Windows / Release / Win32.

  • Client Deathmatch compiles and links cleanly, no new warnings in the touched files.
  • Tests_Client (gtest): 304/304 pass.
  • clang-format produces no diff.

In-game, with a test resource:

  1. setElementStreamDistance(obj, 50) — the object streams out at ~50 units and back in when approaching; confirmed via onClientElementStreamIn / onClientElementStreamOut logging.
  2. setElementStreamDistance(veh, 600) — the vehicle streams in at ~600 and out at ~650 (hysteresis), with no pop-in and no blocking load, confirming the model was pre-cached.
  3. setElementModelStreamDistance(411, 700) applies to all Infernus vehicles; a per-element setElementStreamDistance on one of them wins over the model value, which in turn wins over the type value.
  4. setElementTypeStreamDistance("object", 100) takes effect immediately on already-created objects, and on objects created afterwards.
  5. Passing nil as the distance resets to the inherited value; getElementStreamDistance reports the effective value at each step.
  6. Reconnecting to a different server clears the model/type tables (they live on CClientManager).

Regression checks:

  • On a server that never calls these functions, vehicle/ped/object streaming and nametag range are unchanged, and FPS on a dense map matches the previous build.
  • setElementStreamable, isElementStreamedIn and low-LOD object handover still behave as before.

Checklist

  • Your code should follow the coding guidelines.
  • Smaller pull requests are easier to review. If your pull request is beefy, your pull request should be reviewable commit-by-commit.

- Introduced methods for setting and getting stream distances for both element types and specific models.
- Implemented a mechanism to resolve stream distances based on element type, model, and custom settings.
- Enhanced the CClientManager to iterate over stream elements and refresh their distances when changes occur.
- Updated CClientStreamElement to apply and refresh stream distances, ensuring proper handling of custom distances.
- Added Lua bindings for stream distance functions, allowing script access to set and retrieve stream distances.

This update improves the streaming system's flexibility and performance, ensuring elements are managed more effectively based on their distance from the camera.
Comment on lines +2542 to +2545
static const std::unordered_map<std::string, eClientEntityType> streamableTypes{
{"object", CCLIENTOBJECT}, {"vehicle", CCLIENTVEHICLE}, {"ped", CCLIENTPED},
{"player", CCLIENTPLAYER}, {"pickup", CCLIENTPICKUP}, {"marker", CCLIENTMARKER},
};

@FileEX FileEX Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other types like building, sound, weapon or searchlight? From what I can see, CClientPointLight doesn't handle streaming.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added weapon and searchlight.

CClientWeapon derives from CClientObject and uses the object streamer, but reports CCLIENTWEAPON, so neither the object type override nor a model override was reaching it. It also needs its own RefreshStreamDistance() call, because the base constructor resolves the range while the dynamic type is still CCLIENTOBJECT. CClientSearchLight is a stream element on the light streamer, but the bulk refresh never visited it — it now iterates CClientPointLightsManager::m_SearchLightList.

The rest don't go through CClientStreamer at all, so there's nothing to expose:

  • buildingCClientBuilding is a plain CClientEntity, GTA's building pool handles it
  • soundCClientSound is a plain CClientEntity with its own 3D distance model
  • light — right, CClientPointLights isn't a stream element either

So the list is now object, vehicle, ped, player, pickup, marker, weapon, searchlight. I left a comment on the type table explaining why the others aren't there.

@FileEX FileEX added the enhancement New feature or request label Aug 4, 2026
Both go through CClientStreamer but were missing from the type table.
CClientWeapon is CClientObject derived with its own CCLIENTWEAPON type, so
neither "object" nor a model override reached it, and it needs an extra
RefreshStreamDistance() because the base constructor resolves it as an object.
CClientSearchLight was never visited by the bulk refresh.

Buildings, sounds and lights are plain CClientEntity and are not streamed by
CClientStreamer, so they cannot be supported here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add custom stream distance functions per-element, per-type, and per-modelID

2 participants