343 lines
7.8 KiB
Markdown
343 lines
7.8 KiB
Markdown
# High-FPS Client Plan
|
|
|
|
This document describes how to move the current client from a hard 60 FPS model
|
|
to a safe high-FPS model without breaking gameplay timing.
|
|
|
|
Date of analysis: 2026-04-16
|
|
|
|
## Executive Summary
|
|
|
|
The current client is not limited to 60 FPS by a single config value. It is
|
|
limited by architecture.
|
|
|
|
Current hard constraints in the source:
|
|
|
|
- `src/UserInterface/PythonApplication.cpp`
|
|
- constructor switches `CTimer` into custom time mode
|
|
- main loop advances by a fixed `16/17 ms`
|
|
- main loop sleeps until the next tick
|
|
- `src/EterBase/Timer.cpp`
|
|
- custom mode returns `16 + (m_index & 1)` milliseconds per frame
|
|
- `src/GameLib/GameType.cpp`
|
|
- `g_fGameFPS = 60.0f`
|
|
- `src/GameLib/ActorInstanceMotion.cpp`
|
|
- motion frame math depends on `g_fGameFPS`
|
|
- `src/EterLib/GrpDevice.cpp`
|
|
- presentation interval is also involved, especially in fullscreen
|
|
|
|
Because of this, a simple FPS unlock would likely produce one or more of these
|
|
problems:
|
|
|
|
- broken combo timing
|
|
- incorrect animation frame stepping
|
|
- combat desync with the server
|
|
- accelerated or jittery local effects
|
|
- unstable camera or UI timing
|
|
|
|
The correct design is:
|
|
|
|
1. Keep simulation on a fixed tick.
|
|
2. Decouple rendering from simulation.
|
|
3. Add interpolation for render state.
|
|
4. Expose render cap and VSync as separate options.
|
|
|
|
## Current Timing Model
|
|
|
|
### Main loop
|
|
|
|
In `src/UserInterface/PythonApplication.cpp`:
|
|
|
|
- `CTimer::Instance().UseCustomTime()` is enabled in the constructor.
|
|
- `rkTimer.Advance()` advances simulated time.
|
|
- `GetElapsedMilliecond()` returns a fixed `16/17 ms`.
|
|
- `s_uiNextFrameTime += uiFrameTime` schedules the next frame.
|
|
- `Sleep(rest)` enforces the cap.
|
|
|
|
This means the current process loop is effectively built around a fixed 60 Hz
|
|
clock.
|
|
|
|
### Gameplay coupling
|
|
|
|
`src/GameLib/GameType.cpp` defines:
|
|
|
|
```cpp
|
|
extern float g_fGameFPS = 60.0f;
|
|
```
|
|
|
|
This value is used by motion and attack frame math in:
|
|
|
|
- `src/GameLib/ActorInstanceMotion.cpp`
|
|
- `src/GameLib/RaceMotionData.cpp`
|
|
|
|
The result is that update timing and motion timing are coupled to 60 FPS.
|
|
|
|
## Design Target
|
|
|
|
Target architecture:
|
|
|
|
- simulation update: fixed 60 Hz
|
|
- render: uncapped or capped to monitor / user value
|
|
- interpolation: enabled between fixed simulation states
|
|
- VSync: optional
|
|
- gameplay legality: unchanged from the server point of view
|
|
|
|
This lets the client render at:
|
|
|
|
- 120 FPS
|
|
- 144 FPS
|
|
- 165 FPS
|
|
- 240 FPS
|
|
- uncapped
|
|
|
|
while still keeping game logic deterministic.
|
|
|
|
## Recommended Implementation
|
|
|
|
### Step 1: Replace the custom fixed timer for frame scheduling
|
|
|
|
Do not delete all timer code immediately. First isolate responsibilities.
|
|
|
|
Split timing into:
|
|
|
|
- simulation accumulator time
|
|
- render frame delta
|
|
- presentation pacing
|
|
|
|
Recommended source touchpoints:
|
|
|
|
- `src/EterBase/Timer.cpp`
|
|
- `src/EterBase/Timer.h`
|
|
- `src/UserInterface/PythonApplication.cpp`
|
|
|
|
Preferred clock source:
|
|
|
|
- `QueryPerformanceCounter` / `QueryPerformanceFrequency`
|
|
- or a StepTimer-style wrapper with high-resolution real time
|
|
|
|
### Step 2: Convert the main loop to fixed update + variable render
|
|
|
|
Current model:
|
|
|
|
- one process pass
|
|
- one fixed pseudo-frame
|
|
- optional sleep
|
|
|
|
Target model:
|
|
|
|
```text
|
|
read real delta
|
|
accumulate delta
|
|
while accumulator >= fixedTick:
|
|
run simulation update
|
|
accumulator -= fixedTick
|
|
alpha = accumulator / fixedTick
|
|
render(alpha)
|
|
present
|
|
optional sleep for render cap
|
|
```
|
|
|
|
Recommended fixed tick:
|
|
|
|
- `16.666666 ms`
|
|
|
|
Recommended initial render caps:
|
|
|
|
- 60
|
|
- 120
|
|
- 144
|
|
- 165
|
|
- 240
|
|
- unlimited
|
|
|
|
### Step 3: Keep gameplay update on fixed 60 Hz
|
|
|
|
Do not increase gameplay tick in the first rollout.
|
|
|
|
Keep these systems on the fixed update path:
|
|
|
|
- network processing that depends on gameplay state
|
|
- actor motion update
|
|
- attack state transitions
|
|
- player movement simulation
|
|
- effect simulation when tied to gameplay
|
|
- camera state that depends on actor state
|
|
|
|
This reduces risk dramatically.
|
|
|
|
### Step 4: Render from interpolated state
|
|
|
|
The renderer must not directly depend on only the last fixed update result if
|
|
you want smooth motion at high refresh rates.
|
|
|
|
Add previous and current renderable state for:
|
|
|
|
- actor transform
|
|
- mount transform
|
|
- camera transform
|
|
- optionally important effect anchors
|
|
|
|
At render time:
|
|
|
|
- interpolate position
|
|
- slerp or interpolate rotation where needed
|
|
- use `alpha` from the simulation accumulator
|
|
|
|
Without this step, high-FPS output will still look like 60 FPS motion with extra
|
|
duplicate frames.
|
|
|
|
### Step 5: Separate render cap from VSync
|
|
|
|
The current D3D present settings are not enough on their own.
|
|
|
|
Recommended behavior:
|
|
|
|
- windowed:
|
|
- allow `immediate` plus optional software frame cap
|
|
- fullscreen:
|
|
- keep VSync configurable
|
|
- do not rely on fullscreen `PresentationInterval` as the only limiter
|
|
|
|
Source touchpoint:
|
|
|
|
- `src/EterLib/GrpDevice.cpp`
|
|
|
|
### Step 6: Make `SetFPS()` real or remove it
|
|
|
|
`app.SetFPS()` exists today but only stores `m_iFPS`. It does not drive the main
|
|
loop.
|
|
|
|
Source touchpoints:
|
|
|
|
- `src/UserInterface/PythonApplication.cpp`
|
|
- `src/UserInterface/PythonApplication.h`
|
|
- `src/UserInterface/PythonApplicationModule.cpp`
|
|
|
|
Required action:
|
|
|
|
- wire `m_iFPS` into the render pacing code
|
|
- or rename the API to reflect the real behavior
|
|
|
|
### Step 7: Audit all 60 FPS assumptions
|
|
|
|
Search and review every system that assumes 60 FPS timing.
|
|
|
|
Known touchpoints:
|
|
|
|
- `src/GameLib/GameType.cpp`
|
|
- `src/GameLib/ActorInstanceMotion.cpp`
|
|
- `src/GameLib/RaceMotionData.cpp`
|
|
- `src/AudioLib/MaSoundInstance.h`
|
|
- `src/AudioLib/SoundEngine.cpp`
|
|
- `src/AudioLib/Type.cpp`
|
|
|
|
Expected rule:
|
|
|
|
- gameplay timing stays fixed at 60 Hz for rollout 1
|
|
- render-only timing becomes variable
|
|
- audio smoothing constants may need review if they are implicitly tied to frame
|
|
rate instead of elapsed time
|
|
|
|
## Rollout Plan
|
|
|
|
### Phase 1: Mechanical decoupling
|
|
|
|
- introduce high-resolution real timer
|
|
- add accumulator-based fixed update loop
|
|
- keep simulation at 60 Hz
|
|
- render once per outer loop
|
|
|
|
Deliverable:
|
|
|
|
- no visible gameplay behavior change at 60 Hz cap
|
|
|
|
### Phase 2: Interpolation
|
|
|
|
- store previous and current render states
|
|
- render with interpolation alpha
|
|
- validate player, NPC, mount, and camera smoothness
|
|
|
|
Deliverable:
|
|
|
|
- motion looks smoother above 60 Hz
|
|
|
|
### Phase 3: Render settings
|
|
|
|
- implement `render_fps_limit`
|
|
- implement `vsync` toggle
|
|
- expose settings to Python and config
|
|
|
|
Deliverable:
|
|
|
|
- user-selectable 120 / 144 / 165 / 240 / unlimited
|
|
|
|
### Phase 4: Validation
|
|
|
|
Test matrix:
|
|
|
|
- 60 Hz monitor, VSync on and off
|
|
- 144 Hz monitor, cap 60 / 144 / unlimited
|
|
- minimized and alt-tab paths
|
|
- crowded city combat
|
|
- boss fight
|
|
- mounted combat
|
|
- loading transitions
|
|
- packet-heavy scenes
|
|
|
|
Metrics:
|
|
|
|
- stable attack cadence
|
|
- no combo timing regression
|
|
- no movement speed regression
|
|
- no animation stalls
|
|
- no camera jitter
|
|
- no CPU runaway when capped
|
|
|
|
## Risks
|
|
|
|
### High risk
|
|
|
|
- attack and combo logic tied to frame count
|
|
- animation transitions tied to frame count
|
|
- server-visible timing drift
|
|
|
|
### Medium risk
|
|
|
|
- effect systems that assume one render per update
|
|
- audio systems with frame-based smoothing
|
|
- UI code that assumes stable fixed frame cadence
|
|
|
|
### Lower risk
|
|
|
|
- D3D present configuration
|
|
- config plumbing for user settings
|
|
|
|
## Minimal Safe First Patch
|
|
|
|
If the goal is the fastest safe path, the first implementation should do only
|
|
this:
|
|
|
|
1. keep gameplay at fixed 60 Hz
|
|
2. render more often
|
|
3. interpolate actor and camera transforms
|
|
4. expose render cap option
|
|
|
|
Do not attempt in the first patch:
|
|
|
|
- 120 Hz gameplay simulation
|
|
- rewriting all motion logic to time-based math
|
|
- changing server combat timing
|
|
|
|
## Success Criteria
|
|
|
|
- client can render above 60 FPS on high refresh displays
|
|
- gameplay remains server-compatible
|
|
- no measurable change in combat legality
|
|
- 60 FPS mode still behaves like the current client
|
|
|
|
## Recommended Next Step
|
|
|
|
Implement Phase 1 and Phase 2 together in a branch.
|
|
|
|
That is the smallest meaningful change set that can produce visibly smoother
|
|
output without committing to a full time-based gameplay rewrite.
|