DeltaLoaderDocs
  1. Home
  2. API Wiki
  3. sw/render.hpp

sw/render.hpp

Frame end, OpenGL entry points, shaders, uniforms, render targets, the deferred pipeline.

Source: include/sw/render.hpp

render.hpp
// render.hpp - the graphics engine of the game: where a frame ends, which OpenGL calls it
// uses, and where the post-processing sits. A base for upscalers and render mods.
//
// What is proven (REPORT 3.111; static):
//
//  * The game renders with OpenGL 3+ through a statically linked GLFW (3.3 style). The window
//    context has a table of function pointers: GLFW's swapBuffers is 0x90740 (it ends in
//    SwapBuffers(window + 0x270)) and makeCurrent is 0x906c0; the table is filled by 0x90f90
//    at window + 0x240.. . The swap is the ONE place where a frame ends.
//  * All OpenGL functions beyond 1.1 are loaded by one stub each (2,591 names); the pointer
//    is kept in a slot. gl_function() returns what the game loaded. 59 of them are used by
//    the engine; the 43 functions of OpenGL 1.1 come through the import table of OPENGL32.dll.
//  * The renderer object (constructor 0x6d16c0) holds 80 shader objects at fixed offsets.
//    The frame function 0x6d4de0 (36 KB) runs, in the order of its code: the depth downsample
//    and SSAO, the scene, then temporal AA (shader temporal_aa), motion blur, tonemap and
//    FXAA. The engine already has what a temporal upscaler needs: a velocity texture
//    (texture_velocity), the depth, the colour of the last frame and a per-frame offset
//    (rand_offset, the jitter) - see shader_inputs in the mapping.
//
// THE PIPELINE (REPORT 3.113). The renderer is deferred. One view ("pipeline", 0x66e3a0 builds it)
// owns these targets: [0] the G-buffer (RGBA16F, RGBA8, RG16F = the VELOCITY, depth/stencil),
// [1] the lit scene colour (the `light` shader composes the G-buffer into it), [2] the history
// of the last frame, [3] the output of the anti-aliasing, then SSAO, bloom, and small ones. The
// anti-aliasing mode is a field of the pipeline (1 = FXAA, 2 = temporal AA). In mode 2 the pass
// 0x68fde0 reads [1] (colour), [0] attachment 2 (velocity) and [0] depth, the history [2] and
// writes [3]; the tonemap then reads [3] and DRAWS TO THE SCREEN (framebuffer 0) at the window
// size. There is no separate final blit. An upscaler therefore goes between the scene colour
// and the tonemap: read upscaler_inputs(), write `output_texture`. upscaler_inputs() delivers
// exactly the OpenGL ids a temporal upscaler needs. The jitter is NOT in the engine:
// `rand_offset` of the TAA is two CRT rand() values (noise for the filter), the projection
// is not jittered; a temporal upscaler has to add its own.
//
//   #include "sw/render.hpp"
//
//   sw::render::watch();                                 // once, in sw_mod_start
//   sw::render::on_frame_end(my_overlay, nullptr);       // runs on the render thread, GL context current
//   void* fn = sw::render::gl_function("glBindFramebuffer");
//
// RENDER TARGETS (REPORT 3.112). Every framebuffer of the renderer is an object of the family
// mm_resource_target_* (target, multiple, multisample, mipmapped). watch_targets() hooks the
// functions that create and delete their OpenGL objects and keeps the list, so targets()
// returns every live target with its OpenGL framebuffer, colour and depth texture ids, size
// and format. The size of the main view is the client area of the window; there is no render
// scale setting (graphics state + 0x8f0 / 0x8f4). The last use of the scene target is a blit
// to framebuffer 0 (slot 4 of the class); that is the place for an upscaler to take over.
//
// ABOUT DLSS. NVIDIA's DLSS SDK has no OpenGL backend (Direct3D 11/12 and Vulkan only), and
// this game is OpenGL. So there are three honest ways: (1) a spatial upscaler written as an
// OpenGL shader in the place of the FXAA pass (FSR 1 style; needs nothing the engine lacks);
// (2) a temporal upscaler (FSR 2 style) in the place of temporal_aa, which gets the velocity,
// depth and jitter the engine already has; (3) DLSS itself through a shared texture between
// OpenGL and Vulkan/D3D (GL_EXT_memory_object or WGL_NV_DX_interop2) - heavy, and the colour,
// depth and velocity have to be copied out and the result back. This header gives the base
// for all three: the frame end, the GL entry points, the pass order and the shader offsets.
// It does not contain an upscaler.

#include "sw/generated_gl.hpp"
#include "sw/generated_render.hpp"

#include <cstddef>
#include <cstdint>
#include <vector>

namespace sw::render {

// Called once per frame, on the render thread, with the GL context current, just before the
// frame is presented. `window` is the GLFW window of the game.
using FrameCallback = void (*)(void* window, void* user);

// Hooks the swap (0x90740). False if the game does not match, the hook fails, or this is the
// 32 bit build.
bool watch();

// The swap was called in the last moments.
bool ready();

// Adds a callback for the end of a frame. False before watch(), or when the same
// callback/user pair is already there.
bool on_frame_end(FrameCallback callback, void* user);

// Removes all callbacks of this module.
void remove_callbacks();

// The number of frames presented since watch(), and the time between the last two
// presents in milliseconds.
std::uint64_t frames();
double frame_ms();

// The window of the last frame and its device context (window + 0x270).
void* window();
void* window_dc();

// The entry of the table for this name, or nullptr. Names are the OpenGL names
// ("glBindFramebuffer"), as in the mapping gl_functions.tsv.
const GlEntry* gl_entry(const char* name);

// The function pointer the game holds for this name (what wglGetProcAddress returned, or the
// entry of OPENGL32.dll), or nullptr when the name is unknown or the game has not loaded it.
void* gl_function(const char* name);

// How many places of the engine call this function (0 = loaded but never used).
int gl_uses(const char* name);

// The viewport of the current framebuffer (glGetIntegerv(GL_VIEWPORT)). Only valid inside a
// frame callback (GL context current on this thread).
bool viewport(int& x, int& y, int& width, int& height);

// Your own shader uniform. The game sets only the uniforms its C++ shader classes know, but a uniform that you add to
// a shader (a mod folder with the edited .glsl) keeps the value you give it: uniforms belong to the program object.
// set_uniform() finds every program that declares `name` and writes the value into it (1 to 4 floats; or one int).
// Call it from a frame callback (render thread, GL context current), for example once a frame or when the value changes.
// Returns the number of programs that took the value (0: no program declares the name, or not watching).
// The program that was bound is bound again afterwards. Not tried in a running game.
int set_uniform(const char* name, const float* values, int count);
int set_uniform_int(const char* name, int value);

// ---- render targets and the size of the view --------------------------------------------------
enum class TargetKind { target, multiple, multisample, mipmapped };

// The format enum the renderer uses (REPORT 3.112) and what it is in OpenGL.
enum class TargetFormat : int {
    none = 0,
    depth24 = 1,           // GL_DEPTH_COMPONENT24
    depth24_shadow = 2,    // GL_DEPTH_COMPONENT24, linear filter, compare mode
    depth24_stencil8 = 3,  // GL_DEPTH24_STENCIL8
    rgba8 = 4,
    rgba16f = 5,
    rgba32f = 6,
    r8 = 7,
    r16f = 8,
    rg8 = 9,
    rg16f = 10,
};

// The OpenGL internal format (GL_RGBA16F = 0x881A, ...) of a format, or 0.
unsigned gl_internal_format(TargetFormat format);
// Bytes per pixel of a colour format, 0 for depth formats and none.
int bytes_per_pixel(TargetFormat format);

struct Attachment {
    unsigned texture = 0;
    int index = 0;
    TargetFormat format = TargetFormat::none;
};

struct TargetInfo {
    std::uintptr_t address = 0;
    TargetKind kind = TargetKind::target;
    unsigned fbo = 0;                 // the OpenGL framebuffer object
    int width = 0, height = 0;
    unsigned color_texture = 0;       // first colour attachment (0 = none)
    TargetFormat color_format = TargetFormat::none;
    unsigned depth_texture = 0;
    TargetFormat depth_format = TargetFormat::none;
    int attachments = 0;              // colour attachments of a "multiple" target, else 1 or 0
    std::vector<Attachment> colors;   // all colour attachments (one for a single target)
};

// Hooks the create and delete functions of the target classes (8 functions). False if the
// game does not match or a hook fails.
bool watch_targets();

// Every live target seen since watch_targets() (targets created before it are not known).
std::vector<TargetInfo> targets();

// Reads one target object; false if the address is not a target of the family.
bool read_target(std::uintptr_t address, TargetInfo& out);

// Hooks the update of the view size (0x1be550) so that the graphics state object is known.
bool watch_view();

// The size of the frame buffer / of the window client area as the renderer has it, and the
// aspect ratio. False before the first update. The two sizes are equal in this build.
bool framebuffer_size(int& width, int& height);
bool window_size(int& width, int& height);
float aspect();

// ---- the pipeline of a view and the inputs of an upscaler ---------------------------------------
// The members of the pipeline object, by index (offset = 8 * index): each one points at a target.
enum class PipelineSlot : int {
    gbuffer = 0,        // mm_resource_target_multiple: RGBA16F, RGBA8, RG16F (velocity), depth/stencil
    scene = 1,          // RGBA16F + depth/stencil: the lit scene colour; input of the AA
    history = 2,        // RGBA16F + depth: the output of the last frame (texture_color_last_frame)
    aa_output = 3,      // RGBA16F + depth/stencil: output of TAA / input of FXAA; the tonemap reads it in TAA mode
    ssao = 4,           // R8
    ssao_blur = 5,      // R8
    bloom_a = 6,        // RGBA16F at the size / bloom divisor
    bloom_b = 7,
    depth_copy = 8,     // R16F + depth/stencil, only when the pipeline flag +0xc8 is set
    mipmapped = 0xf,
    shadow_small = 0x10,   // 128 x 128, depth only
    linear_depth = 0x11,   // R16F + depth/stencil, full size: handed to the scene passes
    shadow_large = 0x12,   // 1024 x 1024 R16F
    reflection_a = 0x16,   // 512 x 512 RGBA16F
    reflection_b = 0x17,
};

enum class AaMode : int { none = 0, fxaa = 1, temporal = 2 };

struct Pipeline {
    std::uintptr_t address = 0;
    AaMode aa_mode = AaMode::none;
    bool motion_blur = false;
    std::vector<TargetInfo> slots;   // indexed by PipelineSlot (missing = address 0)
    const TargetInfo& slot(PipelineSlot s) const;
};

// What a temporal upscaler reads and writes (OpenGL texture ids and sizes), taken from the pipeline.
struct UpscalerInputs {
    unsigned color_texture = 0;       // the lit scene colour (RGBA16F), pipeline [1]
    unsigned velocity_texture = 0;    // G-buffer attachment 2 (RG16F)
    unsigned depth_texture = 0;       // G-buffer depth/stencil
    unsigned history_texture = 0;     // pipeline [2]
    unsigned output_texture = 0;      // pipeline [3]: where the tonemap reads in TAA mode
    unsigned output_fbo = 0;
    int input_width = 0, input_height = 0;       // size of the scene colour
    int output_width = 0, output_height = 0;     // size of the output target (equal in this build)
    AaMode aa_mode = AaMode::none;
    bool velocity_is_rg16f = false;   // the format check that tells the velocity attachment is what it should be
};

// Hooks the frame function (0x6d4de0) so that the renderer object is known.
bool watch_renderer();

// The renderer object (nullptr before the first frame after watch_renderer()).
void* renderer();

// The pipeline of a view: 0 = the main view (renderer + 0x2628), 1 = the second view (+0x2718).
bool pipeline(int view, Pipeline& out);

// The inputs of an upscaler for the main view; false if the pipeline is not complete.
bool upscaler_inputs(UpscalerInputs& out);

// The shader object of the renderer for a shader name ("temporal_aa"): its offset inside
// the renderer object, or 0 when there is none.
std::uint32_t renderer_shader_offset(const char* shader);

// ---- addresses (RVA, stormworks64.exe 1.15.25) -------------------------------------------------
inline constexpr std::uint32_t kRvaGlfwContextInit = 0x90f90;     // fills the function table of the context
inline constexpr std::uint32_t kRvaGlfwMakeCurrent = 0x906c0;     // table + 0x240
inline constexpr std::uint32_t kRvaGlfwSwapBuffers = 0x90740;     // table + 0x248; ends in SwapBuffers(dc)
inline constexpr std::size_t kOffContextMakeCurrent = 0x240;
inline constexpr std::size_t kOffContextSwapBuffers = 0x248;
inline constexpr std::size_t kOffWindowDc = 0x270;                // window + 0x270 = the device context

// Render target classes (vtable RVAs), the factories on the resource manager, and the fields.
inline constexpr std::uint32_t kVtableTargetBase = 0xb11770;
inline constexpr std::uint32_t kVtableTarget = 0xb0a778;
inline constexpr std::uint32_t kVtableTargetMultiple = 0xb5c4d8;
inline constexpr std::uint32_t kVtableTargetMultisample = 0xb0cd40;
inline constexpr std::uint32_t kVtableTargetMipmapped = 0xb06980;
inline constexpr std::uint32_t kRvaCreateTarget = 0x1b2630;            // (manager, w, h, colour, depth, name)
inline constexpr std::uint32_t kRvaCreateTargetMultiple = 0x1b2940;
inline constexpr std::uint32_t kRvaCreateTargetMultisample = 0x1b2ad0;
inline constexpr std::uint32_t kRvaCreateTargetMipmapped = 0x1b27c0;
inline constexpr std::uint32_t kRvaTargetCreateGl[4] = {0x1ae530, 0x1aec00, 0x1af2a0, 0x1af6a0};   // slot 1
inline constexpr std::uint32_t kRvaTargetDeleteGl[4] = {0x1ae680, 0x1aedd0, 0x1af340, 0x1af7d0};   // slot 2
inline constexpr std::uint32_t kRvaTargetBlit = 0x1ae700;              // slot 4 of mm_resource_target
inline constexpr std::uint32_t kRvaRenderTargetsCreate = 0x66e3a0;     // creates the targets of one view
inline constexpr std::uint32_t kRvaRenderTargetsCreateB = 0x66ec40;
inline constexpr std::uint32_t kRvaUpdateViewportSize = 0x1be550;      // GetClientRect -> graphics state
inline constexpr std::size_t kOffTargetMemory = 0xc;
inline constexpr std::size_t kOffTargetFbo = 0x28;
inline constexpr std::size_t kOffTargetWidth = 0x2c;
inline constexpr std::size_t kOffTargetHeight = 0x30;
inline constexpr std::size_t kOffTargetColorTexture = 0x48;
inline constexpr std::size_t kOffTargetColorFormat = 0x4c;
inline constexpr std::size_t kOffTargetDepthTexture = 0x50;
inline constexpr std::size_t kOffTargetDepthFormat = 0x54;
inline constexpr std::size_t kOffTargetMultipleColors = 0x48;          // data pointer of the ring (0xc bytes each)
inline constexpr std::size_t kOffTargetMultipleCapacity = 0x50;
inline constexpr std::size_t kOffTargetMultipleHead = 0x54;
inline constexpr std::size_t kOffTargetMultipleCount = 0x58;
inline constexpr std::size_t kOffTargetMultipleDepthTexture = 0x60;
inline constexpr std::size_t kOffTargetMultipleDepthFormat = 0x68;
inline constexpr std::size_t kOffGraphicsFramebufferWidth = 0x8f0;
inline constexpr std::size_t kOffGraphicsFramebufferHeight = 0x8f4;
inline constexpr std::size_t kOffGraphicsWindowWidth = 0x8f8;
inline constexpr std::size_t kOffGraphicsWindowHeight = 0x8fc;
inline constexpr std::size_t kOffGraphicsAspect = 0x900;

// The pipeline and the anti-aliasing passes.
inline constexpr std::size_t kOffRendererPipelineMain = 0x2628;        // the pipeline object inside the renderer
inline constexpr std::size_t kOffRendererPipelineSecond = 0x2718;
inline constexpr std::size_t kOffPipelineMotionBlur = 0xc9;            // byte: motion blur on
inline constexpr std::size_t kOffPipelineAaMode = 0xcc;                // int: 1 = FXAA, 2 = temporal AA
inline constexpr std::size_t kOffPipelineBloomDivisor = 0xe4;
inline constexpr std::uint32_t kRvaTaaSetup = 0x68fcb0;                // (shader, camera): program, sampler units, matrix, rand_offset
inline constexpr std::uint32_t kRvaTaaDraw = 0x68fde0;                 // (shader, quad, colour target, G-buffer, history target)
inline constexpr std::uint32_t kRvaTonemapSetup = 0x68e670;
inline constexpr std::uint32_t kRvaTonemapDraw = 0x68e8b0;
inline constexpr std::uint32_t kRvaFxaaDraw = 0x68f8c0;
inline constexpr std::uint32_t kRvaMotionBlurDraw = 0x6902d0;
inline constexpr std::uint32_t kRvaBindScreen = 0x6ddba0;              // binds framebuffer 0 and sets the viewport to the window size
inline constexpr std::uint32_t kRvaTargetAddColor = 0x1aee70;          // mm_resource_target_multiple::add_color_attachment(format)
inline constexpr std::size_t kOffRendererLight = 0x17b8;               // the `light` shader object (deferred composition)

inline constexpr std::uint32_t kRvaRendererCtor = 0x6d16c0;       // builds the shader objects (4297 bytes)
inline constexpr std::uint32_t kRvaFrameRender = 0x6d4de0;         // the frame: passes in code order (36279 bytes)
inline constexpr std::uint32_t kRvaFramebufferGroup = 0x1adee0;    // the render target helpers 0x1adee0..0x1af800
inline constexpr std::uint32_t kRvaFramebufferBlit1 = 0x1ae700;    // glBlitFramebuffer
inline constexpr std::uint32_t kRvaFramebufferBlit2 = 0x1aeef0;
inline constexpr std::uint32_t kRvaFramebufferBlit3 = 0x1af800;

// Offsets of the post-processing shader objects inside the renderer object.
inline constexpr std::size_t kOffRendererTonemap = 0x1488;
inline constexpr std::size_t kOffRendererSsao = 0x1598;
inline constexpr std::size_t kOffRendererSsaoBlur = 0x1658;
inline constexpr std::size_t kOffRendererFxaa = 0x16b8;
inline constexpr std::size_t kOffRendererTemporalAa = 0x16f0;
inline constexpr std::size_t kOffRendererMotionBlur = 0x1760;
inline constexpr std::size_t kOffRendererDownsampleDepth = 0x1e60;

}  // namespace sw::render

Documentation for game build 1.15.26. Found a mistake? Open an issue.