Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ nix develop -c cmake --build build/linux
cd build/linux && nix develop <repo> -c ctest
```

For day-to-day work `nix develop -c cmake --preset linux-dev` (then build
`build/linux-dev`) builds every module as its own `.so`, so an edit relinks one
library. Benchmarks and captures you compare stay on `build/linux`.

The binary needs the dev shell at runtime too (`libvkd3d`), so prefix runs with
`nix develop -c`, including read-only ones like `--validate` and `--dump-schema`.

Expand Down
25 changes: 11 additions & 14 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ option(RX_MIMALLOC "Route allocations through mimalloc (+memory tracking)" ON)
option(RX_INSTALL "Generate install()/export rules for find_package(rx)"
${PROJECT_IS_TOP_LEVEL})

# The "DLL test": build each rx module as a shared library instead of a static
# archive. Off by default (the static build is the shipping configuration). This
# is a correctness discipline, not a distribution mode: hidden visibility plus
# explicit per-module export annotations (foundation/build_config/export.h) document each
# module's real cross-DSO API and flush out ambient-state bugs.
option(RX_SHARED "Build rx modules as shared libraries (experimental)" OFF)
# The component build: each rx module is a shared library instead of a static
# archive, so an edit relinks one .so. The `linux-dev` preset turns it on for
# day-to-day work; the static build stays the shipping and benchmark
# configuration. Hidden visibility plus explicit per-module export annotations
# (foundation/build_config/export.h) keep each module's cross-DSO API honest.
option(RX_SHARED "Build rx modules as shared libraries (the -dev presets)" OFF)

if(RX_SHARED)
if(RX_INSTALL)
Expand All @@ -63,12 +63,8 @@ if(RX_SHARED)
# Third-party static archives (equilibrium::base, volk, VMA, Jolt, kinema, NRD,
# FSR3) get linked into rx shared objects, so their objects must be PIC.
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
message(WARNING
"RX_SHARED is experimental. rx modules build as shared objects with hidden "
"visibility + explicit export annotations. Caveat: equilibrium's base::Option "
"registry (base::InitChain) is a per-DSO instance under hidden visibility, so "
"each option-owning DSO re-applies env overrides at init. "
"Env knobs work; the mechanism differs from the static single-registry build.")
# One base::Option / base::Feature registry per process, not per DSO: base's
# InitChain carries default visibility (base/containers/init_chain.h).
else()
set(RX_LIB_TYPE STATIC)
endif()
Expand Down Expand Up @@ -190,8 +186,9 @@ if(NOT TARGET volk)
# whole table instead is worse: it collides with SDL3's own Vulkan symbols.
# The correct model is a fully-private volk copy per DSO that uses it, each
# initialized by that DSO. Force hidden visibility so no volk symbol leaks
# out to interpose another DSO or SDL. (render initializes its copy in
# vk_device.cc; the viewer initializes the imgui copy, see debug_ui.cc.)
# out to interpose another DSO or SDL. rx::gpu fills its copy in
# vk_device.cc; every other DSO fills its own the first time it calls
# gpu::GetVulkanHandles (rxe/gpu/rhi/vulkan_interop.h).
set_target_properties(volk PROPERTIES
C_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
Expand Down
30 changes: 26 additions & 4 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,42 @@
"name": "linux",
"binaryDir": "${sourceDir}/build/linux",
"generator": "Ninja",
"cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" }
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "linux-dev",
"inherits": "linux",
"binaryDir": "${sourceDir}/build/linux-dev",
"cacheVariables": {
"RX_SHARED": "ON",
"RX_INSTALL": "OFF"
}
},
{
"name": "windows",
"binaryDir": "${sourceDir}/build/windows",
"generator": "Visual Studio 17 2022",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "macos",
"binaryDir": "${sourceDir}/build/macos",
"generator": "Ninja",
"cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", "RX_FETCH_SDL3": "ON" },
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"RX_FETCH_SDL3": "ON"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
}
]
}
42 changes: 26 additions & 16 deletions docs/STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,22 +331,29 @@ out of scope.

| preset | `RX_SHARED` | use |
|---|---|---|
| `linux-dev`, `windows-dev` | ON | day-to-day work: one DSO per module |
| `linux-dev` (`build/linux-dev`) | ON | day-to-day work: one DSO per module |
| `linux`, `windows`, `android`, ... | OFF | shipping, benchmarks, install/export |

Benchmarks keep running on the static presets, so the shared build never skews
a performance number.

Before `RX_SHARED` can be the default for development, anything that must exist
once per process has to live in exactly one DSO:

- the `base::Option` registry (today one instance per DSO, see the CMake
warning): export it from `foundation` or build equilibrium base shared
- the allocator override (`memory/new_override.cc`) and mimalloc; on Windows,
the shared CRT (`/MD`) everywhere
- log sinks, the feature list and the app identity
- volk: under `RX_SHARED`, volk is built as its own shared library so every
`_vulkan.cc` file sees one loaded function table
Benchmarks and captures keep running on the static presets, so the shared build
never skews a performance number.

What has to exist once per process, and how the shared build keeps it so:

- `base::Option` / `base::Feature` registry: equilibrium's `InitChain` and its
item types carry default visibility, so the chain root is one ELF-unique
symbol however many DSOs instantiate it. Before, an option set from a
platform tier's `[options]` was invisible outside the DSO that declared it.
- volk: it cannot be shared (its table uses the Vulkan entry point names, which
collide with SDL3's), so each DSO keeps a private copy. `rx::gpu` fills its
own at device creation; every other DSO fills its copy inside
`gpu::GetVulkanHandles`, which every Vulkan interop caller already calls
first.
- log sinks, the feature table, app identity and the memory tracker live in
`rx_foundation` and are reached through exported functions; the allocator
override is compiled into the executable only (`rx_enable_mimalloc`).
- Windows: PE has no unique symbols, so a `windows-dev` preset first needs
equilibrium's base linked as a DLL and the shared CRT (`/MD`) everywhere. Not
done yet.

## Violations to fix before moving files

Expand Down Expand Up @@ -413,8 +420,11 @@ and after.
`rx::gpu::d3d12`, `rx::gpu::null`); window, events and the imgui renderer
into `rx::ui`; `WorldClock` into `rx::app`, `exr_write` into `rx::asset`.
The include lint now checks namespaces too.
5. **Development shared build.** Add the single-instance fixes and the `-dev`
presets, then per-module feature flags on the single registry.
5. **Development shared build.**
- 5a (done): the whole tree links as shared objects (missing exports
added), one option registry per process, per-DSO volk filled through
`GetVulkanHandles`, and the `linux-dev` preset.
- 5b: per-module feature flags on the single registry.
6. **Plugins and apps.** Add `RX_PLUGIN`, `PluginRegistry` and `rx_add_app`.
`runtime/` becomes `apps/shell/`, the tools move into `apps/`, and
`EngineContext` goes away.
Expand Down
8 changes: 0 additions & 8 deletions runtime/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,6 @@ int main(int argc, char** argv) {
const bool verify_shot = !config.shot_path.empty() && !::getenv("RX_UI_SHOT_SEQ");
if (verify_shot) rx::fs::Remove(config.shot_path);

#if defined(RX_SHARED_BUILD)
// The viewer's own base::Option knobs (viewer.cc, camera_input.cc,
// demo_scenes.cc, debug_ui.cc) live on this executable's InitChain, which
// under RX_SHARED is a separate instance from the engine DSOs' chains. Apply
// the executable's env overrides here; each engine DSO applies its own at
// subsystem init. Compiled out in the static build (one shared chain).
base::InitOptionsFromEnv();
#endif

rx::Viewer viewer(config);
rx::app::Host host;
Expand Down
5 changes: 3 additions & 2 deletions rxe/anim/anim_graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "base/memory/move.h"
#include "base/memory/unique_pointer.h"
#include "base/strings/string_ref.h"
#include "foundation/build_config/export.h"
#include "foundation/build_config/types.h"
#include "rxe/asset/skeleton.h"

Expand All @@ -22,7 +23,7 @@ struct GraphState;
// The graph is authored kinema-side (kinema stays a private detail of
// rxe/anim, so it never appears in this header); BuildBipedLocomotionGraph is
// the first such archetype factory.
class AnimGraph {
class RX_ANIM_EXPORT AnimGraph {
public:
AnimGraph();
explicit AnimGraph(base::UniquePointer<internal::GraphState> state);
Expand Down Expand Up @@ -53,7 +54,7 @@ class AnimGraph {
// forward root motion), a 1D walk<->run blend space on a "speed" parameter, and
// a two-state machine (idle <-> locomotion) whose transitions are driven by
// "speed". The result is shared by every RigPlayer of this rig.
AnimGraph BuildBipedLocomotionGraph(const asset::Skeleton& skeleton);
RX_ANIM_EXPORT AnimGraph BuildBipedLocomotionGraph(const asset::Skeleton& skeleton);

} // namespace rx::anim

Expand Down
3 changes: 2 additions & 1 deletion rxe/anim/foot_placement.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "base/functional/function.h"
#include "base/memory/unique_pointer.h"
#include "foundation/build_config/export.h"
#include "foundation/math/math.h"
#include "rxe/anim/anim_graph.h"
#include "rxe/anim/pose.h"
Expand All @@ -23,7 +24,7 @@ using GroundProbe = base::Function<bool(const Vec3& origin, Vec3* hit, Vec3* nor
// and the L/R hip/knee/ankle of its biped rig), then Apply()'d each frame after
// RigPlayer::Update. Owns its model-space scratch, so the per-frame path never
// allocates.
class FootPlacement {
class RX_ANIM_EXPORT FootPlacement {
public:
FootPlacement();
~FootPlacement();
Expand Down
3 changes: 2 additions & 1 deletion rxe/anim/rig_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "base/functional/function.h"
#include "base/memory/unique_pointer.h"
#include "base/strings/string_ref.h"
#include "foundation/build_config/export.h"
#include "foundation/math/math.h"
#include "rxe/anim/anim_graph.h"
#include "rxe/anim/pose.h"
Expand All @@ -19,7 +20,7 @@ namespace rx::anim {
//
// No heap allocation happens after Bind: the arena and buffers are sized once,
// and the SoA pose stays SoA until the caller skins it.
class RigPlayer {
class RX_ANIM_EXPORT RigPlayer {
public:
RigPlayer();
~RigPlayer();
Expand Down
3 changes: 2 additions & 1 deletion rxe/asset/exr_write.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


#include "base/strings/xstring.h"
#include "foundation/build_config/export.h"
#include "foundation/build_config/types.h"

namespace rx::asset {
Expand All @@ -11,7 +12,7 @@ namespace rx::asset {
// is width*height*3 floats, row-major, top row first. Returns false on a file
// error. Minimal but spec-correct: enough for compositors (Nuke, Resolve, oiio,
// ffmpeg) to read the engine's linear-hdr captures as a production container.
bool WriteExrRgbF32(const base::String& path, u32 width, u32 height, const f32* rgb);
RX_ASSET_EXPORT bool WriteExrRgbF32(const base::String& path, u32 width, u32 height, const f32* rgb);

} // namespace rx::asset

Expand Down
2 changes: 1 addition & 1 deletion rxe/asset/vfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace rx::asset {

// A source of files: a loose directory, an .rxp pack, a BSA, a BA2. Providers
// are mounted into the Vfs in priority order.
class FileProvider {
class RX_ASSET_EXPORT FileProvider {
public:
virtual ~FileProvider() = default;

Expand Down
3 changes: 2 additions & 1 deletion rxe/audio/audio_clip.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "base/containers/vector.h"
#include "base/memory/unique_pointer.h"
#include "base/strings/string_ref.h"
#include "foundation/build_config/export.h"
#include "foundation/build_config/types.h"

namespace rx::audio {
Expand Down Expand Up @@ -48,7 +49,7 @@ class Decoder {
// magic-number fallback (a .fuz is really a wrapped xWMA, a mod's loose file may
// lie about its extension). `bytes` is copied into the decoder, so the caller's
// buffer need not outlive it. Null when no decoder handles the data.
base::UniquePointer<Decoder> OpenDecoder(ByteSpan bytes, base::StringRef extension);
RX_AUDIO_EXPORT base::UniquePointer<Decoder> OpenDecoder(ByteSpan bytes, base::StringRef extension);

// Decodes `bytes` completely into an AudioClip. Returns an invalid clip (see
// AudioClip::valid) when the format is unsupported or the data is malformed.
Expand Down
7 changes: 0 additions & 7 deletions rxe/audio/audio_system.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ base::StringRef ExtensionOf(base::StringRef path) {
} // namespace

bool AudioSystem::Initialize(asset::Vfs* vfs) {
#if defined(RX_SHARED_BUILD)
// Apply this DSO's env option overrides: under RX_SHARED base::Option's
// registry is per-DSO (hidden-visibility InitChain head), so the app's
// InitOptionsFromEnv() never reaches the audio DSO's options. See the same
// note in Renderer::Initialize. Compiled out in the static build.
base::InitOptionsFromEnv();
#endif
vfs_ = vfs;
muted_ = Mute.get();
master_ = rx::Clamp(Volume.get(), 0.0f, 1.0f);
Expand Down
5 changes: 3 additions & 2 deletions rxe/gpu/rhi/device.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "base/functional/function.h"
#include "base/memory/unique_pointer.h"
#include "base/strings/xstring.h"
#include "foundation/build_config/export.h"
#include "foundation/build_config/types.h"
#include "rxe/gpu/rhi/bindings.h"
#include "rxe/gpu/rhi/command_list.h"
Expand All @@ -29,7 +30,7 @@ enum class Backend : u8 {
// renderer's control flow testable without a GPU or a loader
};

const char* BackendName(Backend backend);
RX_GPU_EXPORT const char* BackendName(Backend backend);

struct DeviceDesc {
Backend backend = Backend::kAuto;
Expand Down Expand Up @@ -110,7 +111,7 @@ enum class PresentResult : u8 {
// a null-backend device (is_stub() true) comes back when no loader, no
// capable GPU or no presentable window is available, and every operation on
// it is a safe no-op.
class Device {
class RX_GPU_EXPORT Device {
public:
static constexpr u32 kMaxFramesInFlight = 2;

Expand Down
3 changes: 2 additions & 1 deletion rxe/gpu/rhi/gpu_profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <base/containers/vector.h>

#include "base/strings/xstring.h"
#include "foundation/build_config/export.h"
#include "foundation/build_config/types.h"
#include "rxe/gpu/rhi/types.h"

Expand All @@ -23,7 +24,7 @@ class Device;
// reports the count, and each slot grows to that size the next time it comes
// round (a pool can only be replaced where its last submission has retired),
// so the miss lasts at most one cycle through the slots.
class GpuProfiler {
class RX_GPU_EXPORT GpuProfiler {
public:
struct PassTiming {
base::String name;
Expand Down
3 changes: 2 additions & 1 deletion rxe/gpu/rhi/types.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef RX_RENDER_RHI_TYPES_H_
#define RX_RENDER_RHI_TYPES_H_

#include "foundation/build_config/export.h"
#include "foundation/build_config/types.h"

// Backend-agnostic value types shared by the whole RHI. Nothing in this header
Expand Down Expand Up @@ -72,7 +73,7 @@ constexpr bool IsDepthFormat(Format format) {

// Bytes per texel (per block for BC formats), for footprint estimates and
// tightly packed copies.
u32 FormatTexelBytes(Format format);
RX_GPU_EXPORT u32 FormatTexelBytes(Format format);

struct Extent2D {
u32 width = 0;
Expand Down
27 changes: 26 additions & 1 deletion rxe/gpu/rhi/vulkan_interop.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
namespace rx::gpu {

struct VulkanHandles {
// The loader entry point the device was created through; what another DSO
// needs to fill its own volk table.
PFN_vkGetInstanceProcAddr get_instance_proc_addr = nullptr;
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice physical_device = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
Expand All @@ -27,8 +30,30 @@ struct VulkanHandles {
VmaAllocator allocator = nullptr;
};

// Null-filled when `device` is not the Vulkan backend. Callers use
// GetVulkanHandles below instead.
RX_GPU_EXPORT VulkanHandles QueryVulkanHandles(Device& device);

// Null-filled when `device` is not the Vulkan backend.
RX_GPU_EXPORT VulkanHandles GetVulkanHandles(Device& device);
//
// Under RX_SHARED every DSO carries its own private volk table (see the top-level
// CMakeLists for why it cannot be shared), and only the device's own DSO fills
// its copy. Every interop consumer asks for the handles before its first vk*
// call, so this is the one place to fill the calling module's copy: inline and
// hidden, it runs in the caller's DSO against that DSO's table, once per device.
inline VulkanHandles GetVulkanHandles(Device& device) {
VulkanHandles handles = QueryVulkanHandles(device);
#if defined(RX_SHARED_BUILD)
static VkDevice loaded = VK_NULL_HANDLE;
if (handles.device != VK_NULL_HANDLE && handles.device != loaded) {
volkInitializeCustom(handles.get_instance_proc_addr);
volkLoadInstanceOnly(handles.instance);
volkLoadDevice(handles.device);
loaded = handles.device;
}
#endif
return handles;
}

// Frames-in-flight the renderer cycles (== the number of distinct frame slots a
// SceneHookContext::frame_slot can take). An app sizing per-frame resources for
Expand Down
Loading