From bfe52a81f90f5657245db1168df4b268be78d3cd Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 10:37:08 +0200 Subject: [PATCH] Add high-FPS render pacing and telemetry --- docs/anti-cheat-architecture-2026.md | 307 ++++++++ docs/high-fps-client-plan.md | 342 ++++++++ src/EterBase/Timer.cpp | 5 + src/EterBase/Timer.h | 3 +- src/GameLib/ActorInstance.cpp | 28 + src/GameLib/ActorInstance.h | 3 + src/UserInterface/InstanceBase.cpp | 15 + src/UserInterface/InstanceBase.h | 4 +- src/UserInterface/PythonApplication.cpp | 777 +++++++++++++------ src/UserInterface/PythonApplication.h | 35 +- src/UserInterface/PythonCharacterManager.cpp | 34 + src/UserInterface/PythonCharacterManager.h | 1 + src/UserInterface/PythonSystem.cpp | 37 + src/UserInterface/PythonSystem.h | 9 +- src/UserInterface/PythonSystemModule.cpp | 35 + 15 files changed, 1402 insertions(+), 233 deletions(-) create mode 100644 docs/anti-cheat-architecture-2026.md create mode 100644 docs/high-fps-client-plan.md diff --git a/docs/anti-cheat-architecture-2026.md b/docs/anti-cheat-architecture-2026.md new file mode 100644 index 0000000..db2dba1 --- /dev/null +++ b/docs/anti-cheat-architecture-2026.md @@ -0,0 +1,307 @@ +# Anti-Cheat Architecture 2026 + +This document proposes a practical anti-cheat design for the current Metin2 +client and server stack. + +Date of analysis: 2026-04-16 + +## Executive Summary + +The current client contains only a weak integrity layer: + +- `src/UserInterface/ProcessCRC.cpp` computes CRC values for the running binary. +- `src/UserInterface/PythonNetworkStream.cpp` calls `BuildProcessCRC()` when the + client enters select phase. +- `src/UserInterface/PythonNetworkStreamPhaseGame.cpp` injects two CRC bytes + into attack packets. +- `src/UserInterface/PythonNetworkStreamPhaseGame.cpp` contains a `CG::HACK` + reporting path. +- `src/UserInterface/ProcessScanner.cpp` exists, but it is not wired into the + live runtime. +- `src/UserInterface/PythonNetworkStreamPhaseGame.cpp::__SendCRCReportPacket()` + is currently a stub that returns `true`. + +This is not a production-grade anti-cheat. It raises the bar slightly for basic +binary tampering, but it does not defend well against modern external tools, +automation, packet abuse, or VM-assisted cheats. + +The recommended 2026 design is: + +1. Make the server authoritative for critical combat and movement rules. +2. Add telemetry and delayed-ban workflows. +3. Add a commercial client anti-cheat as a hardening layer, not as the primary + trust model. +4. Treat botting and farming as a separate abuse problem with separate signals. + +## Goals + +- Stop speedhack, combohack, rangehack, teleport, packet spam, and most forms of + client-side state forgery. +- Raise the cost of memory editing, DLL injection, and automation. +- Reduce false positives by moving final judgment to server-side evidence. +- Keep the design compatible with a legacy custom client. + +## Non-Goals + +- Absolute cheat prevention. +- Fully trusting a third-party kernel driver to solve game logic abuse. +- Real-time automatic bans on a single weak signal. + +## Current State + +### What exists today + +- Client self-CRC: + - `src/UserInterface/ProcessCRC.cpp` +- CRC pieces attached to outgoing attack packets: + - `src/UserInterface/PythonNetworkStreamPhaseGame.cpp` +- Hack message queue and packet transport: + - `src/UserInterface/PythonNetworkStreamPhaseGame.cpp` + +### What is missing today + +- No active process blacklist or scanner integration in the runtime path. +- No complete CRC reporting flow to the server. +- No server-authoritative verification model documented in the current client + source tree. +- No robust evidence pipeline for review, delayed bans, or behavioral scoring. + +## Recommended Architecture + +### Layer 1: Server-Authoritative Core + +This is the most important layer. The server must become the source of truth for +combat and movement outcomes. + +### Movement validation + +The server should validate: + +- maximum velocity by actor state +- acceleration spikes +- position delta against elapsed server time +- warp-only transitions +- path continuity through collision or map rules +- impossible Z transitions and fly transitions + +Recommended outputs: + +- hard reject for impossible moves +- suspicion score for repeated marginal violations +- session telemetry record + +### Combat validation + +The server should validate: + +- attack rate by skill and weapon class +- combo timing windows +- skill cooldowns +- victim distance and angle +- hit count per server tick +- target existence and visibility rules +- animation-independent skill gate timing + +This removes most of the value of classic combohack, attack speed manipulation, +and packet replay. + +### State validation + +The server should own: + +- HP / SP mutation rules +- buff durations +- item use success +- teleport authorization +- quest-critical transitions +- loot generation and pickup eligibility + +### Layer 2: Telemetry and Evidence + +Do not ban on one event unless the event is impossible by design. + +Collect per-session evidence for: + +- movement violations +- attack interval violations +- repeated rejected packets +- target distance anomalies +- client build mismatch +- CRC mismatch +- anti-cheat vendor verdict +- bot-like repetition and route loops + +Recommended workflow: + +- score each event +- decay score over time +- trigger thresholds for: + - silent logging + - shadow restrictions + - temporary action blocks + - GM review + - delayed ban wave + +Delayed bans are important because instant bans teach attackers which checks +worked. + +### Layer 3: Client Integrity + +Use client integrity only as a supporting layer. + +Recommended client responsibilities: + +- verify binary and pack integrity +- report build identifier and signed manifest identity +- detect common injection or patching signals +- report tampering and environment metadata +- protect transport secrets and runtime config + +Suggested upgrades to the current client: + +- replace the current partial CRC path with a real signed build identity +- sign client packs and executable manifests +- complete `__SendCRCReportPacket()` and send useful integrity evidence +- remove dead anti-cheat code or wire it fully +- add secure telemetry batching instead of ad hoc string-only hack messages + +### Layer 4: Commercial Anti-Cheat + +### Recommended vendor order for this project + +1. `XIGNCODE3` +2. `BattlEye` +3. `Denuvo Anti-Cheat` +4. `Easy Anti-Cheat` + +### Why this order + +`XIGNCODE3` looks like the best functional fit for a legacy MMO client: + +- public positioning is strongly focused on online PC games +- public feature set includes macro detection, resource protection, and real + time pattern updates +- market fit appears closer to older custom launchers and non-mainstream engine + stacks + +`BattlEye` is a strong option if you want a more established premium PC +anti-cheat and can support tighter integration work. + +`Denuvo Anti-Cheat` is technically strong, but it is likely the heaviest vendor + path in both integration and commercial terms. + +`Easy Anti-Cheat` is attractive if budget is the main constraint, but it should +not change the core rule: the server must still be authoritative. + +### Layer 5: Anti-Bot and Economy Abuse + +Botting should be treated as a separate control plane. + +Recommended controls: + +- route repetition heuristics +- farming loop detection +- vendor / warehouse / trade anomaly scoring +- captcha or interaction challenge only after confidence threshold +- account graph analysis for mule and funnel behavior +- hardware and device clustering where legally appropriate + +Do not rely on captcha alone. It is only a friction tool. + +## Proposed Rollout + +### Phase 1: Fix trust boundaries + +- document which outcomes are currently trusted from the client +- move movement and attack legality fully server-side +- add structured telemetry records + +Deliverable: + +- server can reject impossible movement and impossible attack cadence without any + client anti-cheat dependency + +### Phase 2: Replace weak integrity + +- complete binary and pack integrity reporting +- version all reports by client build +- bind reports to account, session, and channel + +Deliverable: + +- reliable client integrity evidence reaches the server + +### Phase 3: Vendor pilot + +- integrate one commercial anti-cheat in observe mode first +- compare vendor verdicts with server suspicion score +- review false positives before enforcement + +Deliverable: + +- enforcement policy based on combined evidence + +### Phase 4: Ban and response pipeline + +- implement delayed ban waves +- implement silent risk tiers +- implement GM review tooling + +Deliverable: + +- repeatable response process instead of ad hoc action + +## Concrete Changes For This Codebase + +Client-side work: + +- `src/UserInterface/ProcessCRC.cpp` + - replace simple CRC flow with signed manifest and richer integrity report +- `src/UserInterface/PythonNetworkStream.cpp` + - send integrity bootstrap at phase transition +- `src/UserInterface/PythonNetworkStreamPhaseGame.cpp` + - implement real CRC and integrity packet submission + - replace free-form hack strings with typed evidence events +- `src/UserInterface/ProcessScanner.cpp` + - either remove dead code or reintroduce it as a fully designed subsystem + +Server-side work: + +- movement validator +- combat cadence validator +- anomaly scoring service +- evidence storage +- review and ban tooling + +## Design Rules + +- Never trust timing sent by the client for legality. +- Never trust client-side cooldown completion. +- Never trust client position as final truth for hit validation. +- Never ban permanently on a single client-only signal. +- Never ship anti-cheat changes without telemetry first. + +## Success Metrics + +- drop in successful speedhack and combohack reports +- drop in impossible movement accepted by the server +- reduced farm bot session length +- reduced economy inflation from automated abuse +- acceptable false positive rate during observe mode + +## Recommended Next Step + +Implement Layer 1 before vendor integration. + +If the server still accepts impossible combat or movement, a commercial +anti-cheat only increases attacker cost. It does not fix the trust model. + +## External References + +- Unity cheat prevention guidance +- Epic Easy Anti-Cheat public developer material +- BattlEye public developer material +- Wellbia XIGNCODE3 public product material +- Denuvo Anti-Cheat public product material +- Metin2Dev discussions around server-side validation, anti-bot workflows, and + weak community anti-cheat releases diff --git a/docs/high-fps-client-plan.md b/docs/high-fps-client-plan.md new file mode 100644 index 0000000..2c2a871 --- /dev/null +++ b/docs/high-fps-client-plan.md @@ -0,0 +1,342 @@ +# 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. diff --git a/src/EterBase/Timer.cpp b/src/EterBase/Timer.cpp index 923b829..875945f 100644 --- a/src/EterBase/Timer.cpp +++ b/src/EterBase/Timer.cpp @@ -65,6 +65,11 @@ VOID ELTimer_SetFrameMSec() gs_dwFrameTime = ELTimer_GetMSec(); } +VOID ELTimer_SetFrameMSecValue(DWORD dwFrameTime) +{ + gs_dwFrameTime = dwFrameTime; +} + CTimer::CTimer() { ELTimer_Init(); diff --git a/src/EterBase/Timer.h b/src/EterBase/Timer.h index 4865232..42051a5 100644 --- a/src/EterBase/Timer.h +++ b/src/EterBase/Timer.h @@ -38,4 +38,5 @@ VOID ELTimer_SetServerMSec(DWORD dwServerTime); DWORD ELTimer_GetServerMSec(); VOID ELTimer_SetFrameMSec(); -DWORD ELTimer_GetFrameMSec(); \ No newline at end of file +VOID ELTimer_SetFrameMSecValue(DWORD dwFrameTime); +DWORD ELTimer_GetFrameMSec(); diff --git a/src/GameLib/ActorInstance.cpp b/src/GameLib/ActorInstance.cpp index c1d5cfe..c8b2bbf 100644 --- a/src/GameLib/ActorInstance.cpp +++ b/src/GameLib/ActorInstance.cpp @@ -18,6 +18,9 @@ void CActorInstance::INSTANCEBASE_Deform() void CActorInstance::INSTANCEBASE_Transform() { + m_v3InterpolationStartPosition = D3DXVECTOR3(m_x, m_y, m_z); + m_fInterpolationStartRotation = m_fcurRotation; + if (m_pkHorse) { m_pkHorse->INSTANCEBASE_Transform(); @@ -44,6 +47,30 @@ void CActorInstance::INSTANCEBASE_Transform() UpdateAttribute(); } +void CActorInstance::ApplyRenderInterpolation(float fInterpolation) +{ + const float fAlpha = std::max(0.0f, std::min(1.0f, fInterpolation)); + + D3DXVECTOR3 v3InterpolatedPosition; + v3InterpolatedPosition.x = m_v3InterpolationStartPosition.x + (m_x - m_v3InterpolationStartPosition.x) * fAlpha; + v3InterpolatedPosition.y = m_v3InterpolationStartPosition.y + (m_y - m_v3InterpolationStartPosition.y) * fAlpha; + v3InterpolatedPosition.z = m_v3InterpolationStartPosition.z + (m_z - m_v3InterpolationStartPosition.z) * fAlpha; + + const float fInterpolatedRotation = GetInterpolatedRotation(m_fInterpolationStartRotation, m_fcurRotation, fAlpha); + + SetPosition(v3InterpolatedPosition); + if (0.0f != m_rotX || 0.0f != m_rotY) + { + CGraphicObjectInstance::SetRotation(m_rotX, m_rotY, fInterpolatedRotation); + } + else + { + CGraphicObjectInstance::SetRotation(fInterpolatedRotation); + } + + Transform(); +} + void CActorInstance::OnUpdate() { @@ -875,6 +902,7 @@ void CActorInstance::__InitializeRotationData() { m_fAtkDirRot = 0.0f; m_fcurRotation = 0.0f; + m_fInterpolationStartRotation = 0.0f; m_rotBegin = 0.0f; m_rotEnd = 0.0f; m_rotEndTime = 0.0f; diff --git a/src/GameLib/ActorInstance.h b/src/GameLib/ActorInstance.h index 25d2856..00a2307 100644 --- a/src/GameLib/ActorInstance.h +++ b/src/GameLib/ActorInstance.h @@ -425,6 +425,7 @@ class CActorInstance : public IActorInstance, public IFlyTargetableObject void LookAt(CActorInstance * pInstance); void LookWith(CActorInstance * pInstance); void LookAtFromXY(float x, float y, CActorInstance * pDestInstance); + void ApplyRenderInterpolation(float fInterpolation); void SetReachScale(float fScale); @@ -767,6 +768,7 @@ class CActorInstance : public IActorInstance, public IFlyTargetableObject float m_x; float m_y; float m_z; + D3DXVECTOR3 m_v3InterpolationStartPosition; D3DXVECTOR3 m_v3Pos; D3DXVECTOR3 m_v3Movement; BOOL m_bNeedUpdateCollision; @@ -779,6 +781,7 @@ class CActorInstance : public IActorInstance, public IFlyTargetableObject // Rotation float m_fcurRotation; + float m_fInterpolationStartRotation; float m_rotBegin; float m_rotEnd; float m_rotEndTime; diff --git a/src/UserInterface/InstanceBase.cpp b/src/UserInterface/InstanceBase.cpp index 5ea6dc0..b54c326 100644 --- a/src/UserInterface/InstanceBase.cpp +++ b/src/UserInterface/InstanceBase.cpp @@ -245,6 +245,15 @@ void CInstanceBase::SHORSE::Render() rkActor.Render(); } +void CInstanceBase::SHORSE::ApplyRenderInterpolation(float fInterpolation) +{ + if (!IsMounting()) + return; + + CActorInstance& rkActor = GetActorRef(); + rkActor.ApplyRenderInterpolation(fInterpolation); +} + void CInstanceBase::__AttachHorseSaddle() { if (!IsMountingHorse()) @@ -1948,6 +1957,12 @@ void CInstanceBase::Transform() m_GraphicThingInstance.INSTANCEBASE_Transform(); } +void CInstanceBase::ApplyRenderInterpolation(float fInterpolation) +{ + m_kHorse.ApplyRenderInterpolation(fInterpolation); + m_GraphicThingInstance.ApplyRenderInterpolation(fInterpolation); +} + void CInstanceBase::Deform() { diff --git a/src/UserInterface/InstanceBase.h b/src/UserInterface/InstanceBase.h index 6c66ba6..3fb0ab2 100644 --- a/src/UserInterface/InstanceBase.h +++ b/src/UserInterface/InstanceBase.h @@ -472,6 +472,7 @@ class CInstanceBase bool UpdateDeleting(); void Transform(); + void ApplyRenderInterpolation(float fInterpolation); void Deform(); void Render(); void RenderTrace(); @@ -847,6 +848,7 @@ class CInstanceBase void SetMoveSpeed(UINT uMovSpd); void Deform(); void Render(); + void ApplyRenderInterpolation(float fInterpolation); CActorInstance& GetActorRef(); CActorInstance* GetActorPtr(); @@ -1139,4 +1141,4 @@ inline int RaceToSex(int race) } return 0; -} \ No newline at end of file +} diff --git a/src/UserInterface/PythonApplication.cpp b/src/UserInterface/PythonApplication.cpp index 61b8ee4..1681b99 100644 --- a/src/UserInterface/PythonApplication.cpp +++ b/src/UserInterface/PythonApplication.cpp @@ -6,12 +6,15 @@ #include "EterGrnLib/Material.h" #include "resource.h" +#include "GameType.h" #include "PythonApplication.h" #include "PythonCharacterManager.h" #include "ProcessScanner.h" #include +#include +#include extern void GrannyCreateSharedDeformBuffer(); extern void GrannyDestroySharedDeformBuffer(); @@ -21,6 +24,91 @@ double g_specularSpd=0.007f; CPythonApplication * CPythonApplication::ms_pInstance; +namespace +{ + // Match the legacy custom timer cadence of alternating 17 ms and 16 ms. + constexpr double kFixedUpdateMS = 16.5; + constexpr double kMaxCatchUpDelayMS = 250.0; + + float ClampInterpolationFactor(float fInterpolation) + { + return std::max(0.0f, std::min(1.0f, fInterpolation)); + } + + D3DXVECTOR3 LerpVector3(const D3DXVECTOR3& c_rv3Begin, const D3DXVECTOR3& c_rv3End, float fInterpolation) + { + return D3DXVECTOR3( + c_rv3Begin.x + (c_rv3End.x - c_rv3Begin.x) * fInterpolation, + c_rv3Begin.y + (c_rv3End.y - c_rv3Begin.y) * fInterpolation, + c_rv3Begin.z + (c_rv3End.z - c_rv3Begin.z) * fInterpolation); + } + + bool IsRenderTelemetryEnabledFromEnvironment() + { + const char* c_szValue = std::getenv("M2_RENDER_TELEMETRY"); + if (!c_szValue || !*c_szValue) + return false; + + if (0 == _stricmp(c_szValue, "1") || 0 == _stricmp(c_szValue, "true") || 0 == _stricmp(c_szValue, "yes") || 0 == _stricmp(c_szValue, "on")) + return true; + + return false; + } + + bool TryReadEnvironmentInt(const char* c_szName, int* piValue) + { + if (!piValue) + return false; + + const char* c_szValue = std::getenv(c_szName); + if (!c_szValue || !*c_szValue) + return false; + + char* pEnd = nullptr; + const long lValue = std::strtol(c_szValue, &pEnd, 10); + if (pEnd == c_szValue || (pEnd && *pEnd)) + return false; + + *piValue = static_cast(lValue); + return true; + } + + void ResetRenderTelemetryTrace() + { + FILE* fp = nullptr; + if (fopen_s(&fp, "log/render_telemetry.txt", "w") != 0 || !fp) + return; + + fprintf(fp, "render telemetry\n"); + fclose(fp); + } + + void AppendRenderTelemetryTrace(const char* c_szFormat, ...) + { + FILE* fp = nullptr; + if (fopen_s(&fp, "log/render_telemetry.txt", "a") != 0 || !fp) + return; + + va_list args; + va_start(args, c_szFormat); + vfprintf(fp, c_szFormat, args); + va_end(args); + fputc('\n', fp); + fclose(fp); + } + + void FormatRenderTargetFPSLabel(unsigned int iFPS, char* c_szBuffer, size_t uBufferSize) + { + if (!c_szBuffer || 0 == uBufferSize) + return; + + if (iFPS > 0) + _snprintf_s(c_szBuffer, uBufferSize, _TRUNCATE, "%u", iFPS); + else + _snprintf_s(c_szBuffer, uBufferSize, _TRUNCATE, "MAX"); + } +} + float c_fDefaultCameraRotateSpeed = 1.5f; float c_fDefaultCameraPitchSpeed = 1.5f; float c_fDefaultCameraZoomSpeed = 0.05f; @@ -35,9 +123,28 @@ m_poMouseHandler(NULL), m_dwUpdateFPS(0), m_dwRenderFPS(0), m_fAveRenderTime(0.0f), +m_bRenderTelemetryEnabled(false), +m_bRenderTelemetryHudVisible(false), +m_dwRenderTelemetryIntervalMS(1000), +m_dwRenderTelemetryWindowStartMS(0), +m_dwRenderTelemetryLoopCount(0), +m_dwRenderTelemetryUpdateCount(0), +m_dwRenderTelemetryRenderCount(0), +m_dwRenderTelemetryBlockedRenderCount(0), +m_dwRenderTelemetryLastPresentTime(0), +m_dwRenderTelemetryPresentGapSamples(0), +m_fRenderTelemetryUpdateTimeSumMS(0.0), +m_fRenderTelemetryRenderTimeSumMS(0.0), +m_fRenderTelemetrySleepTimeSumMS(0.0), +m_fRenderTelemetryInterpolationSum(0.0), +m_fRenderTelemetryPresentGapSumMS(0.0), +m_dwCurRenderTime(0), +m_dwCurUpdateTime(0), +m_dwLoad(0), m_dwFaceCount(0), m_fGlobalTime(0.0f), m_fGlobalElapsedTime(0.0f), +m_fRenderInterpolationFactor(0.0f), m_dwLButtonDownTime(0), m_dwLastIdleTime(0), m_IsMovingMainWindow(false) @@ -54,12 +161,15 @@ m_IsMovingMainWindow(false) m_isWindowFullScreenEnable = FALSE; m_v3CenterPosition = D3DXVECTOR3(0.0f, 0.0f, 0.0f); + m_v3LastCenterPosition = m_v3CenterPosition; m_dwStartLocalTime = ELTimer_GetMSec(); m_tServerTime = 0; m_tLocalStartTime = 0; m_iPort = 0; - m_iFPS = 60; + m_iFPS = std::max(0, m_pySystem.GetRenderFPS()); + m_bRenderTelemetryHudVisible = m_pySystem.IsShowPerformanceHUD(); + InitializeRenderRuntimeOverrides(); m_isActivateWnd = false; m_isMinimizedWnd = true; @@ -168,6 +278,7 @@ void CPythonApplication::RenderGame() float fFarClip = m_pyBackground.GetFarClip(); m_pyGraphic.SetPerspective(30.0f, fAspect, 100.0, fFarClip); + ApplyRenderInterpolation(); CCullingManager::Instance().Process(); @@ -256,10 +367,137 @@ void CPythonApplication::UpdateGame() SetCenterPosition(kPPosMainActor.x, kPPosMainActor.y, kPPosMainActor.z); } +void CPythonApplication::InitializeRenderRuntimeOverrides() +{ + m_bRenderTelemetryEnabled = IsRenderTelemetryEnabledFromEnvironment(); + + int iIntervalMS = 0; + if (TryReadEnvironmentInt("M2_RENDER_TELEMETRY_INTERVAL_MS", &iIntervalMS) && iIntervalMS > 0) + m_dwRenderTelemetryIntervalMS = static_cast(std::min(iIntervalMS, 60000)); + + if (m_bRenderTelemetryEnabled) + { + ResetRenderTelemetryTrace(); + AppendRenderTelemetryTrace("enabled interval_ms=%lu", static_cast(m_dwRenderTelemetryIntervalMS)); + } + + int iRenderFPS = 0; + if (TryReadEnvironmentInt("M2_RENDER_FPS", &iRenderFPS)) + { + m_iFPS = std::max(0, iRenderFPS); + if (m_bRenderTelemetryEnabled) + AppendRenderTelemetryTrace("env_override target_fps=%u", m_iFPS); + } + + char szTargetFPS[16]; + FormatRenderTargetFPSLabel(m_iFPS, szTargetFPS, sizeof(szTargetFPS)); + m_stRenderTelemetrySummary = "Render telemetry\nTarget FPS: "; + m_stRenderTelemetrySummary += szTargetFPS; + m_stRenderTelemetrySummary += "\nCollecting frame pacing..."; + + if (IsRenderTelemetrySamplingEnabled()) + ResetRenderTelemetryWindow(ELTimer_GetMSec()); +} + +void CPythonApplication::ResetRenderTelemetryWindow(DWORD dwNow) +{ + m_dwRenderTelemetryWindowStartMS = dwNow; + m_dwRenderTelemetryLoopCount = 0; + m_dwRenderTelemetryUpdateCount = 0; + m_dwRenderTelemetryRenderCount = 0; + m_dwRenderTelemetryBlockedRenderCount = 0; + m_dwRenderTelemetryPresentGapSamples = 0; + m_fRenderTelemetryUpdateTimeSumMS = 0.0; + m_fRenderTelemetryRenderTimeSumMS = 0.0; + m_fRenderTelemetrySleepTimeSumMS = 0.0; + m_fRenderTelemetryInterpolationSum = 0.0; + m_fRenderTelemetryPresentGapSumMS = 0.0; +} + +void CPythonApplication::FlushRenderTelemetryWindow(DWORD dwNow) +{ + if (!IsRenderTelemetrySamplingEnabled() || !m_dwRenderTelemetryWindowStartMS) + return; + + const DWORD dwWindowMS = std::max(1, dwNow - m_dwRenderTelemetryWindowStartMS); + const double fWindowMS = static_cast(dwWindowMS); + const double fUpdateFPS = static_cast(m_dwRenderTelemetryUpdateCount) * 1000.0 / fWindowMS; + const double fRenderFPS = static_cast(m_dwRenderTelemetryRenderCount) * 1000.0 / fWindowMS; + const double fAvgUpdateMS = m_dwRenderTelemetryUpdateCount ? (m_fRenderTelemetryUpdateTimeSumMS / static_cast(m_dwRenderTelemetryUpdateCount)) : 0.0; + const double fAvgRenderMS = m_dwRenderTelemetryRenderCount ? (m_fRenderTelemetryRenderTimeSumMS / static_cast(m_dwRenderTelemetryRenderCount)) : 0.0; + const double fAvgSleepMS = m_dwRenderTelemetryLoopCount ? (m_fRenderTelemetrySleepTimeSumMS / static_cast(m_dwRenderTelemetryLoopCount)) : 0.0; + const double fAvgInterpolation = m_dwRenderTelemetryRenderCount ? (m_fRenderTelemetryInterpolationSum / static_cast(m_dwRenderTelemetryRenderCount)) : 0.0; + const double fAvgPresentGapMS = m_dwRenderTelemetryPresentGapSamples ? (m_fRenderTelemetryPresentGapSumMS / static_cast(m_dwRenderTelemetryPresentGapSamples)) : 0.0; + const DWORD dwElapsedMS = dwNow - m_dwStartLocalTime; + char szTargetFPS[16]; + char szSummary[256]; + + FormatRenderTargetFPSLabel(m_iFPS, szTargetFPS, sizeof(szTargetFPS)); + _snprintf_s( + szSummary, + sizeof(szSummary), + _TRUNCATE, + "Render %.1f Update %.1f Target %s\nPresent %.1fms Sleep %.1fms CPU %.1f/%.1fms", + fRenderFPS, + fUpdateFPS, + szTargetFPS, + fAvgPresentGapMS, + fAvgSleepMS, + fAvgUpdateMS, + fAvgRenderMS); + m_stRenderTelemetrySummary = szSummary; + + if (m_bRenderTelemetryEnabled) + { + AppendRenderTelemetryTrace( + "elapsed_ms=%lu window_ms=%lu target_fps=%u update_fps=%.2f render_fps=%.2f avg_update_ms=%.2f avg_render_ms=%.2f avg_sleep_ms=%.2f avg_present_gap_ms=%.2f avg_interp=%.3f loops=%lu updates=%lu renders=%lu blocked=%lu cur_update_ms=%lu cur_render_ms=%lu", + static_cast(dwElapsedMS), + static_cast(dwWindowMS), + m_iFPS, + fUpdateFPS, + fRenderFPS, + fAvgUpdateMS, + fAvgRenderMS, + fAvgSleepMS, + fAvgPresentGapMS, + fAvgInterpolation, + static_cast(m_dwRenderTelemetryLoopCount), + static_cast(m_dwRenderTelemetryUpdateCount), + static_cast(m_dwRenderTelemetryRenderCount), + static_cast(m_dwRenderTelemetryBlockedRenderCount), + static_cast(m_dwCurUpdateTime), + static_cast(m_dwCurRenderTime)); + } + + ResetRenderTelemetryWindow(dwNow); +} + +bool CPythonApplication::IsRenderTelemetrySamplingEnabled() const +{ + return m_bRenderTelemetryEnabled || m_bRenderTelemetryHudVisible; +} + +void CPythonApplication::RenderPerformanceHUD() +{ + if (!m_bRenderTelemetryHudVisible) + return; + + CGraphicText* pkDefaultFont = static_cast(DefaultFont_GetResource()); + if (!pkDefaultFont) + return; + + m_kRenderTelemetryTextLine.SetTextPointer(pkDefaultFont); + m_kRenderTelemetryTextLine.SetOutline(true); + m_kRenderTelemetryTextLine.SetMultiLine(true); + m_kRenderTelemetryTextLine.SetColor(1.0f, 1.0f, 1.0f); + m_kRenderTelemetryTextLine.SetPosition(12.0f, 12.0f); + m_kRenderTelemetryTextLine.SetValueString(m_stRenderTelemetrySummary); + m_kRenderTelemetryTextLine.Update(); + m_kRenderTelemetryTextLine.Render(); +} + bool CPythonApplication::Process() { - ELTimer_SetFrameMSec(); - // m_Profiler.Clear(); DWORD dwStart = ELTimer_GetMSec(); @@ -269,6 +507,18 @@ bool CPythonApplication::Process() static DWORD s_dwFaceCount = 0; static UINT s_uiLoad = 0; static DWORD s_dwCheckTime = ELTimer_GetMSec(); + static double s_fNextUpdateTime = static_cast(ELTimer_GetMSec()); + static double s_fNextRenderTime = static_cast(ELTimer_GetMSec()); + static double s_fFixedFrameTime = static_cast(ELTimer_GetMSec()); + + DWORD dwCurrentTime = ELTimer_GetMSec(); + const double fCurrentTime = static_cast(dwCurrentTime); + const bool bSampleRenderTelemetry = IsRenderTelemetrySamplingEnabled(); + if (bSampleRenderTelemetry && !m_dwRenderTelemetryWindowStartMS) + ResetRenderTelemetryWindow(dwCurrentTime); + + if (bSampleRenderTelemetry) + ++m_dwRenderTelemetryLoopCount; if (ELTimer_GetMSec() - s_dwCheckTime > 1000) [[unlikely]] { m_dwUpdateFPS = s_dwUpdateFrameCount; @@ -282,278 +532,314 @@ bool CPythonApplication::Process() s_uiLoad = s_dwFaceCount = s_dwUpdateFrameCount = s_dwRenderFrameCount = 0; } - // Update Time - static BOOL s_bFrameSkip = false; - static UINT s_uiNextFrameTime = ELTimer_GetMSec(); - -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime1=ELTimer_GetMSec(); -#endif - CTimer& rkTimer=CTimer::Instance(); - rkTimer.Advance(); - - m_fGlobalTime = rkTimer.GetCurrentSecond(); - m_fGlobalElapsedTime = rkTimer.GetElapsedSecond(); - - UINT uiFrameTime = rkTimer.GetElapsedMilliecond(); - s_uiNextFrameTime += uiFrameTime; //17 - 1ÃÊ´ç 60fps±âÁØ. - - DWORD updatestart = ELTimer_GetMSec(); -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime2=ELTimer_GetMSec(); -#endif - // Network I/O - m_pyNetworkStream.Process(); - //m_pyNetworkDatagram.Process(); - - m_kGuildMarkUploader.Process(); - - m_kGuildMarkDownloader.Process(); - m_kAccountConnector.Process(); - -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime3=ELTimer_GetMSec(); -#endif - ////////////////////// - // Input Process - // Keyboard - UpdateKeyboard(); -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime4=ELTimer_GetMSec(); -#endif - // Mouse - POINT Point; - if (GetCursorPos(&Point)) [[likely]] { - ScreenToClient(m_hWnd, &Point); - OnMouseMove(Point.x, Point.y); - } - ////////////////////// -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime5=ELTimer_GetMSec(); -#endif - //!@# Alt+Tab Áß SetTransfor ¿¡¼­ ƨ±è Çö»ó ÇØ°áÀ» À§ÇØ - [levites] - //if (m_isActivateWnd) - __UpdateCamera(); -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime6=ELTimer_GetMSec(); -#endif - // Update Game Playing - CResourceManager::Instance().Update(); -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime7=ELTimer_GetMSec(); -#endif - OnCameraUpdate(); -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime8=ELTimer_GetMSec(); -#endif - OnMouseUpdate(); -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime9=ELTimer_GetMSec(); -#endif - OnUIUpdate(); - -#ifdef __PERFORMANCE_CHECK__ - DWORD dwUpdateTime10=ELTimer_GetMSec(); - - if (dwUpdateTime10-dwUpdateTime1>10) - { - static FILE* fp=fopen("perf_app_update.txt", "w"); - - fprintf(fp, "AU.Total %d (Time %d)\n", dwUpdateTime9-dwUpdateTime1, ELTimer_GetMSec()); - fprintf(fp, "AU.TU %d\n", dwUpdateTime2-dwUpdateTime1); - fprintf(fp, "AU.NU %d\n", dwUpdateTime3-dwUpdateTime2); - fprintf(fp, "AU.KU %d\n", dwUpdateTime4-dwUpdateTime3); - fprintf(fp, "AU.MP %d\n", dwUpdateTime5-dwUpdateTime4); - fprintf(fp, "AU.CP %d\n", dwUpdateTime6-dwUpdateTime5); - fprintf(fp, "AU.RU %d\n", dwUpdateTime7-dwUpdateTime6); - fprintf(fp, "AU.CU %d\n", dwUpdateTime8-dwUpdateTime7); - fprintf(fp, "AU.MU %d\n", dwUpdateTime9-dwUpdateTime8); - fprintf(fp, "AU.UU %d\n", dwUpdateTime10-dwUpdateTime9); - fprintf(fp, "----------------------------------\n"); - fflush(fp); - } -#endif - - //UpdateÇϴµ¥ °É¸°½Ã°£.delta°ª - m_dwCurUpdateTime = ELTimer_GetMSec() - updatestart; - - DWORD dwCurrentTime = ELTimer_GetMSec(); - BOOL bCurrentLateUpdate = FALSE; - - s_bFrameSkip = false; - - if (dwCurrentTime > s_uiNextFrameTime) + if (fCurrentTime - s_fNextUpdateTime > kMaxCatchUpDelayMS) { - int dt = dwCurrentTime - s_uiNextFrameTime; - int nAdjustTime = ((float)dt / (float)uiFrameTime) * uiFrameTime; + s_fNextUpdateTime = fCurrentTime; + s_fFixedFrameTime = fCurrentTime; + } - if ( dt >= 500 ) + if (fCurrentTime - s_fNextRenderTime > kMaxCatchUpDelayMS) + s_fNextRenderTime = fCurrentTime; + + int iUpdateCount = 0; + while (fCurrentTime + 0.0001 >= s_fNextUpdateTime) + { + if (!m_isFrameSkipDisable && iUpdateCount >= 5) { - s_uiNextFrameTime += nAdjustTime; - printf("FrameSkip º¸Á¤ %d\n",nAdjustTime); - CTimer::Instance().Adjust(nAdjustTime); + s_fNextUpdateTime = fCurrentTime; + s_fFixedFrameTime = fCurrentTime; + break; } - s_bFrameSkip = true; - bCurrentLateUpdate = TRUE; - } + ++iUpdateCount; + s_fNextUpdateTime += kFixedUpdateMS; + s_fFixedFrameTime += kFixedUpdateMS; + ELTimer_SetFrameMSecValue(static_cast(s_fFixedFrameTime)); + m_v3LastCenterPosition = m_v3CenterPosition; - //s_bFrameSkip = false; - - //if (dwCurrentTime > s_uiNextFrameTime) - //{ - // int dt = dwCurrentTime - s_uiNextFrameTime; - - // //³Ê¹« ´Ê¾úÀ» °æ¿ì µû¶óÀâ´Â´Ù. - // //±×¸®°í m_dwCurUpdateTime´Â deltaÀε¥ delta¶û absolute timeÀ̶û ºñ±³ÇÏ¸é ¾î¼Àڴ°Ü? - // //if (dt >= 500 || m_dwCurUpdateTime > s_uiNextFrameTime) - - // //±âÁ¸ÄÚµå´ë·Î Çϸé 0.5ÃÊ ÀÌÇÏ Â÷À̳­ »óÅ·Πupdate°¡ Áö¼ÓµÇ¸é °è¼Ó rendering frame skip¹ß»ý - // if (dt >= 500 || m_dwCurUpdateTime > s_uiNextFrameTime) - // { - // s_uiNextFrameTime += dt / uiFrameTime * uiFrameTime; - // printf("FrameSkip º¸Á¤ %d\n", dt / uiFrameTime * uiFrameTime); - // CTimer::Instance().Adjust((dt / uiFrameTime) * uiFrameTime); - // s_bFrameSkip = true; - // } - //} - - if (m_isFrameSkipDisable) - s_bFrameSkip = false; - -#ifdef __VTUNE__ - s_bFrameSkip = false; +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime1=ELTimer_GetMSec(); #endif - if (!s_bFrameSkip) - { - // static double pos=0.0f; - // CGrannyMaterial::TranslateSpecularMatrix(fabs(sin(pos)*0.005), fabs(cos(pos)*0.005), 0.0f); - // pos+=0.01f; + CTimer& rkTimer=CTimer::Instance(); + rkTimer.Advance(); + m_fGlobalTime = rkTimer.GetCurrentSecond(); + m_fGlobalElapsedTime = rkTimer.GetElapsedSecond(); + + DWORD updatestart = ELTimer_GetMSec(); +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime2=ELTimer_GetMSec(); +#endif + // Network I/O + m_pyNetworkStream.Process(); + //m_pyNetworkDatagram.Process(); + + m_kGuildMarkUploader.Process(); + + m_kGuildMarkDownloader.Process(); + m_kAccountConnector.Process(); + +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime3=ELTimer_GetMSec(); +#endif + ////////////////////// + // Input Process + // Keyboard + UpdateKeyboard(); +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime4=ELTimer_GetMSec(); +#endif + // Mouse + POINT Point; + if (GetCursorPos(&Point)) [[likely]] { + ScreenToClient(m_hWnd, &Point); + OnMouseMove(Point.x, Point.y); + } + ////////////////////// +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime5=ELTimer_GetMSec(); +#endif + //!@# Alt+Tab Áß SetTransfor ¿¡¼­ ƨ±è Çö»ó ÇØ°áÀ» À§ÇØ - [levites] + //if (m_isActivateWnd) + __UpdateCamera(); +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime6=ELTimer_GetMSec(); +#endif + // Update Game Playing + CResourceManager::Instance().Update(); +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime7=ELTimer_GetMSec(); +#endif + OnCameraUpdate(); +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime8=ELTimer_GetMSec(); +#endif + OnMouseUpdate(); +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime9=ELTimer_GetMSec(); +#endif CGrannyMaterial::TranslateSpecularMatrix(g_specularSpd, g_specularSpd, 0.0f); + OnUIUpdate(); - DWORD dwRenderStartTime = ELTimer_GetMSec(); +#ifdef __PERFORMANCE_CHECK__ + DWORD dwUpdateTime10=ELTimer_GetMSec(); - bool canRender = true; - - if (m_isMinimizedWnd) [[unlikely]] { - canRender = false; - } - else [[likely]] { - if (m_pyGraphic.IsLostDevice()) [[unlikely]] { - CPythonBackground& rkBG = CPythonBackground::Instance(); - rkBG.ReleaseCharacterShadowTexture(); - - if (m_pyGraphic.RestoreDevice()) - rkBG.CreateCharacterShadowTexture(); - else - canRender = false; - } - } - - if (canRender) [[likely]] + if (dwUpdateTime10-dwUpdateTime1>10) { - // RestoreLostDevice - CCullingManager::Instance().Update(); - if (m_pyGraphic.Begin()) [[likely]] { + static FILE* fp=fopen("perf_app_update.txt", "w"); - m_pyGraphic.ClearDepthBuffer(); + fprintf(fp, "AU.Total %d (Time %d)\n", dwUpdateTime9-dwUpdateTime1, ELTimer_GetMSec()); + fprintf(fp, "AU.TU %d\n", dwUpdateTime2-dwUpdateTime1); + fprintf(fp, "AU.NU %d\n", dwUpdateTime3-dwUpdateTime2); + fprintf(fp, "AU.KU %d\n", dwUpdateTime4-dwUpdateTime3); + fprintf(fp, "AU.MP %d\n", dwUpdateTime5-dwUpdateTime4); + fprintf(fp, "AU.CP %d\n", dwUpdateTime6-dwUpdateTime5); + fprintf(fp, "AU.RU %d\n", dwUpdateTime7-dwUpdateTime6); + fprintf(fp, "AU.CU %d\n", dwUpdateTime8-dwUpdateTime7); + fprintf(fp, "AU.MU %d\n", dwUpdateTime9-dwUpdateTime8); + fprintf(fp, "AU.UU %d\n", dwUpdateTime10-dwUpdateTime9); + fprintf(fp, "----------------------------------\n"); + fflush(fp); + } +#endif + + //UpdateÇϴµ¥ °É¸°½Ã°£.delta°ª + m_dwCurUpdateTime = ELTimer_GetMSec() - updatestart; + if (bSampleRenderTelemetry) + { + ++m_dwRenderTelemetryUpdateCount; + m_fRenderTelemetryUpdateTimeSumMS += static_cast(m_dwCurUpdateTime); + } + ++s_dwUpdateFrameCount; + } + + const double fPreviousUpdateTime = s_fNextUpdateTime - kFixedUpdateMS; + m_fRenderInterpolationFactor = ClampInterpolationFactor(static_cast((fCurrentTime - fPreviousUpdateTime) / kFixedUpdateMS)); + + DWORD dwRenderStartTime = ELTimer_GetMSec(); + bool canRender = true; + bool didRender = false; + + if (m_isMinimizedWnd) [[unlikely]] { + canRender = false; + } + else [[likely]] { + if (m_pyGraphic.IsLostDevice()) [[unlikely]] { + CPythonBackground& rkBG = CPythonBackground::Instance(); + rkBG.ReleaseCharacterShadowTexture(); + + if (m_pyGraphic.RestoreDevice()) + rkBG.CreateCharacterShadowTexture(); + else + canRender = false; + } + } + + if (canRender) [[likely]] + { + // RestoreLostDevice + CCullingManager::Instance().Update(); + if (m_pyGraphic.Begin()) [[likely]] { + + m_pyGraphic.ClearDepthBuffer(); #ifdef _DEBUG - m_pyGraphic.SetClearColor(0.3f, 0.3f, 0.3f); - m_pyGraphic.Clear(); + m_pyGraphic.SetClearColor(0.3f, 0.3f, 0.3f); + m_pyGraphic.Clear(); #endif - ///////////////////// - // Interface - m_pyGraphic.SetInterfaceRenderState(); + ///////////////////// + // Interface + m_pyGraphic.SetInterfaceRenderState(); - OnUIRender(); - OnMouseRender(); - ///////////////////// + OnUIRender(); + RenderPerformanceHUD(); + OnMouseRender(); + ///////////////////// - m_pyGraphic.End(); + m_pyGraphic.End(); + m_pyGraphic.Show(); - //DWORD t1 = ELTimer_GetMSec(); - m_pyGraphic.Show(); - //DWORD t2 = ELTimer_GetMSec(); + DWORD dwRenderEndTime = ELTimer_GetMSec(); + didRender = true; - DWORD dwRenderEndTime = ELTimer_GetMSec(); + static DWORD s_dwRenderCheckTime = dwRenderEndTime; + static DWORD s_dwRenderRangeTime = 0; + static DWORD s_dwRenderRangeFrame = 0; - static DWORD s_dwRenderCheckTime = dwRenderEndTime; - static DWORD s_dwRenderRangeTime = 0; - static DWORD s_dwRenderRangeFrame = 0; + m_dwCurRenderTime = dwRenderEndTime - dwRenderStartTime; + s_dwRenderRangeTime += m_dwCurRenderTime; + ++s_dwRenderRangeFrame; - m_dwCurRenderTime = dwRenderEndTime - dwRenderStartTime; - s_dwRenderRangeTime += m_dwCurRenderTime; - ++s_dwRenderRangeFrame; + if (dwRenderEndTime-s_dwRenderCheckTime>1000) [[unlikely]] { + m_fAveRenderTime=float(double(s_dwRenderRangeTime)/double(s_dwRenderRangeFrame)); - if (dwRenderEndTime-s_dwRenderCheckTime>1000) [[unlikely]] { - m_fAveRenderTime=float(double(s_dwRenderRangeTime)/double(s_dwRenderRangeFrame)); + s_dwRenderCheckTime=ELTimer_GetMSec(); + s_dwRenderRangeTime=0; + s_dwRenderRangeFrame=0; + } - s_dwRenderCheckTime=ELTimer_GetMSec(); - s_dwRenderRangeTime=0; - s_dwRenderRangeFrame=0; - } + DWORD dwCurFaceCount=m_pyGraphic.GetFaceCount(); + m_pyGraphic.ResetFaceCount(); + s_dwFaceCount += dwCurFaceCount; - DWORD dwCurFaceCount=m_pyGraphic.GetFaceCount(); - m_pyGraphic.ResetFaceCount(); - s_dwFaceCount += dwCurFaceCount; + if (dwCurFaceCount > 5000) + { + m_dwFaceAccCount += dwCurFaceCount; + m_dwFaceAccTime += m_dwCurRenderTime; - if (dwCurFaceCount > 5000) + m_fFaceSpd=(m_dwFaceAccCount/m_dwFaceAccTime); + + // °Å¸® ÀÚµ¿ Á¶Àý + if (-1 == m_iForceSightRange) { - m_dwFaceAccCount += dwCurFaceCount; - m_dwFaceAccTime += m_dwCurRenderTime; - - m_fFaceSpd=(m_dwFaceAccCount/m_dwFaceAccTime); - - // °Å¸® ÀÚµ¿ Á¶Àý - if (-1 == m_iForceSightRange) - { - static float s_fAveRenderTime = 16.0f; - float fRatio=0.3f; - s_fAveRenderTime=(s_fAveRenderTime*(100.0f-fRatio)+std::max(16.0f, (float)m_dwCurRenderTime)*fRatio)/100.0f; + static float s_fAveRenderTime = 16.0f; + float fRatio=0.3f; + s_fAveRenderTime=(s_fAveRenderTime*(100.0f-fRatio)+std::max(16.0f, (float)m_dwCurRenderTime)*fRatio)/100.0f; - float fFar=25600.0f; - float fNear=MIN_FOG; - double dbAvePow=double(1000.0f/s_fAveRenderTime); - double dbMaxPow=60.0; - float fDistance=std::max((float)(fNear+(fFar-fNear)*(dbAvePow)/dbMaxPow), fNear); - m_pyBackground.SetViewDistanceSet(0, fDistance); - } - // °Å¸® °­Á¦ ¼³Á¤½Ã - else - { - m_pyBackground.SetViewDistanceSet(0, float(m_iForceSightRange)); - } + float fFar=25600.0f; + float fNear=MIN_FOG; + double dbAvePow=double(1000.0f/s_fAveRenderTime); + double dbMaxPow=60.0; + float fDistance=std::max((float)(fNear+(fFar-fNear)*(dbAvePow)/dbMaxPow), fNear); + m_pyBackground.SetViewDistanceSet(0, fDistance); } + // °Å¸® °­Á¦ ¼³Á¤½Ã else { - // 10000 Æú¸®°ï º¸´Ù ÀûÀ»¶§´Â °¡Àå ¸Ö¸® º¸ÀÌ°Ô ÇÑ´Ù - m_pyBackground.SetViewDistanceSet(0, 25600.0f); + m_pyBackground.SetViewDistanceSet(0, float(m_iForceSightRange)); + } + } + else + { + // 10000 Æú¸®°ï º¸´Ù ÀûÀ»¶§´Â °¡Àå ¸Ö¸® º¸ÀÌ°Ô ÇÑ´Ù + m_pyBackground.SetViewDistanceSet(0, 25600.0f); + } + + ++s_dwRenderFrameCount; + + if (bSampleRenderTelemetry) + { + ++m_dwRenderTelemetryRenderCount; + m_fRenderTelemetryRenderTimeSumMS += static_cast(m_dwCurRenderTime); + m_fRenderTelemetryInterpolationSum += static_cast(m_fRenderInterpolationFactor); + + if (m_dwRenderTelemetryLastPresentTime) + { + m_fRenderTelemetryPresentGapSumMS += static_cast(dwRenderEndTime - m_dwRenderTelemetryLastPresentTime); + ++m_dwRenderTelemetryPresentGapSamples; } - ++s_dwRenderFrameCount; + m_dwRenderTelemetryLastPresentTime = dwRenderEndTime; } } } - int rest = s_uiNextFrameTime - ELTimer_GetMSec(); + if (bSampleRenderTelemetry && !didRender) + ++m_dwRenderTelemetryBlockedRenderCount; - if (rest > 0 && !bCurrentLateUpdate ) + if (m_iFPS > 0) { - s_uiLoad -= rest; // ½® ½Ã°£Àº ·Îµå¿¡¼­ »«´Ù.. - Sleep(rest); - } + const double fRenderFrameMS = 1000.0 / static_cast(m_iFPS); + s_fNextRenderTime += fRenderFrameMS; - ++s_dwUpdateFrameCount; + const double fSleepMS = s_fNextRenderTime - static_cast(ELTimer_GetMSec()); + if (fSleepMS > 0.999) + { + const UINT uiSleepMS = static_cast(fSleepMS); + s_uiLoad -= std::min(s_uiLoad, uiSleepMS); + const DWORD dwSleepStart = ELTimer_GetMSec(); + Sleep(static_cast(fSleepMS)); + if (bSampleRenderTelemetry) + m_fRenderTelemetrySleepTimeSumMS += static_cast(ELTimer_GetMSec() - dwSleepStart); + } + } + else + { + s_fNextRenderTime = static_cast(ELTimer_GetMSec()); + } s_uiLoad += ELTimer_GetMSec() - dwStart; - //m_Profiler.ProfileByScreen(); + + const DWORD dwTelemetryNow = ELTimer_GetMSec(); + if (bSampleRenderTelemetry && dwTelemetryNow - m_dwRenderTelemetryWindowStartMS >= m_dwRenderTelemetryIntervalMS) + FlushRenderTelemetryWindow(dwTelemetryNow); + + //m_Profiler.ProfileByScreen(); return true; } +void CPythonApplication::ApplyRenderInterpolation() +{ + const float fInterpolation = ClampInterpolationFactor(m_fRenderInterpolationFactor); + + m_kChrMgr.ApplyRenderInterpolation(fInterpolation); + + if (CAMERA_MODE_NORMAL != m_iCameraMode) + return; + + if (0.0f != m_kCmrPos.m_fViewDir || 0.0f != m_kCmrPos.m_fCrossDir || 0.0f != m_kCmrPos.m_fUpDir) + return; + + CCamera* pCurrentCamera = CCameraManager::Instance().GetCurrentCamera(); + if (!pCurrentCamera) + return; + + const D3DXVECTOR3 v3CenterPosition = LerpVector3(m_v3LastCenterPosition, m_v3CenterPosition, fInterpolation); + const float fDistance = pCurrentCamera->GetDistance(); + const float fPitch = pCurrentCamera->GetPitch(); + const float fRotation = pCurrentCamera->GetRoll(); + + m_pyGraphic.SetPositionCamera( + v3CenterPosition.x, + v3CenterPosition.y, + v3CenterPosition.z + pCurrentCamera->GetTargetHeight(), + fDistance, + fPitch, + fRotation); +} + void CPythonApplication::UpdateClientRect() { RECT rcApp; @@ -1023,7 +1309,39 @@ float CPythonApplication::GetGlobalElapsedTime() void CPythonApplication::SetFPS(int iFPS) { - m_iFPS = iFPS; + m_iFPS = std::max(0, iFPS); + char szTargetFPS[16]; + FormatRenderTargetFPSLabel(m_iFPS, szTargetFPS, sizeof(szTargetFPS)); + m_stRenderTelemetrySummary = "Render telemetry\nTarget FPS: "; + m_stRenderTelemetrySummary += szTargetFPS; + m_stRenderTelemetrySummary += "\nCollecting frame pacing..."; + if (IsRenderTelemetrySamplingEnabled()) + ResetRenderTelemetryWindow(ELTimer_GetMSec()); + + if (m_bRenderTelemetryEnabled) + { + AppendRenderTelemetryTrace( + "set_fps elapsed_ms=%lu target_fps=%u", + static_cast(ELTimer_GetMSec() - m_dwStartLocalTime), + m_iFPS); + } +} + +void CPythonApplication::SetPerformanceHUDVisible(bool isVisible) +{ + m_bRenderTelemetryHudVisible = isVisible; + if (IsRenderTelemetrySamplingEnabled()) + ResetRenderTelemetryWindow(ELTimer_GetMSec()); + else + m_dwRenderTelemetryWindowStartMS = 0; + + if (m_bRenderTelemetryEnabled) + { + AppendRenderTelemetryTrace( + "set_hud elapsed_ms=%lu visible=%u", + static_cast(ELTimer_GetMSec() - m_dwStartLocalTime), + m_bRenderTelemetryHudVisible ? 1u : 0u); + } } int CPythonApplication::GetWidth() @@ -1075,6 +1393,7 @@ void CPythonApplication::Destroy() // SphereMap CGrannyMaterial::DestroySphereMap(); + m_kRenderTelemetryTextLine.Destroy(); m_kWndMgr.Destroy(); CPythonSystem::Instance().SaveConfig(); diff --git a/src/UserInterface/PythonApplication.h b/src/UserInterface/PythonApplication.h index d10658f..0fd3db0 100644 --- a/src/UserInterface/PythonApplication.h +++ b/src/UserInterface/PythonApplication.h @@ -4,6 +4,7 @@ #include "eterLib/Input.h" #include "eterLib/Profiler.h" #include "eterLib/GrpDevice.h" +#include "EterLib/GrpTextInstance.h" #include "eterLib/NetDevice.h" #include "eterLib/GrpLightManager.h" #include "eterLib/GameThreadPool.h" @@ -42,7 +43,13 @@ #include "AbstractApplication.h" #include "MovieMan.h" -#include +struct IGraphBuilder; +struct IBaseFilter; +struct ISampleGrabber; +struct IMediaControl; +struct IMediaEventEx; +struct IVideoWindow; +struct IBasicVideo; class CPythonApplication : public CMSApplication, public CInputKeyboard, public IAbstractApplication { @@ -206,6 +213,7 @@ class CPythonApplication : public CMSApplication, public CInputKeyboard, public float GetPitch(); void SetFPS(int iFPS); + void SetPerformanceHUDVisible(bool isVisible); void SetServerTime(time_t tTime); time_t GetServerTime(); time_t GetServerTimeStamp(); @@ -303,6 +311,12 @@ class CPythonApplication : public CMSApplication, public CInputKeyboard, public BOOL __IsContinuousChangeTypeCursor(int iCursorNum); void __UpdateCamera(); + void ApplyRenderInterpolation(); + bool IsRenderTelemetrySamplingEnabled() const; + void InitializeRenderRuntimeOverrides(); + void ResetRenderTelemetryWindow(DWORD dwNow); + void FlushRenderTelemetryWindow(DWORD dwNow); + void RenderPerformanceHUD(); void __SetFullScreenWindow(HWND hWnd, DWORD dwWidth, DWORD dwHeight, DWORD dwBPP); void __MinimizeFullScreenWindow(HWND hWnd, DWORD dwWidth, DWORD dwHeight); @@ -357,9 +371,28 @@ class CPythonApplication : public CMSApplication, public CInputKeyboard, public PyObject * m_poMouseHandler; D3DXVECTOR3 m_v3CenterPosition; + D3DXVECTOR3 m_v3LastCenterPosition; + CGraphicTextInstance m_kRenderTelemetryTextLine; + std::string m_stRenderTelemetrySummary; unsigned int m_iFPS; + float m_fRenderInterpolationFactor; float m_fAveRenderTime; + bool m_bRenderTelemetryEnabled; + bool m_bRenderTelemetryHudVisible; + DWORD m_dwRenderTelemetryIntervalMS; + DWORD m_dwRenderTelemetryWindowStartMS; + DWORD m_dwRenderTelemetryLoopCount; + DWORD m_dwRenderTelemetryUpdateCount; + DWORD m_dwRenderTelemetryRenderCount; + DWORD m_dwRenderTelemetryBlockedRenderCount; + DWORD m_dwRenderTelemetryLastPresentTime; + DWORD m_dwRenderTelemetryPresentGapSamples; + double m_fRenderTelemetryUpdateTimeSumMS; + double m_fRenderTelemetryRenderTimeSumMS; + double m_fRenderTelemetrySleepTimeSumMS; + double m_fRenderTelemetryInterpolationSum; + double m_fRenderTelemetryPresentGapSumMS; DWORD m_dwCurRenderTime; DWORD m_dwCurUpdateTime; DWORD m_dwLoad; diff --git a/src/UserInterface/PythonCharacterManager.cpp b/src/UserInterface/PythonCharacterManager.cpp index 21b9211..5f6bb91 100644 --- a/src/UserInterface/PythonCharacterManager.cpp +++ b/src/UserInterface/PythonCharacterManager.cpp @@ -358,6 +358,34 @@ struct FCharacterManagerCharacterInstanceDeform //pInstance->Update(); } }; +struct FCharacterManagerCharacterInstanceApplyRenderInterpolation +{ + explicit FCharacterManagerCharacterInstanceApplyRenderInterpolation(float fInterpolation) + : m_fInterpolation(fInterpolation) + { + } + + inline void operator () (const std::pair& cr_Pair) + { + cr_Pair.second->ApplyRenderInterpolation(m_fInterpolation); + } + + float m_fInterpolation; +}; +struct FCharacterManagerCharacterInstanceListApplyRenderInterpolation +{ + explicit FCharacterManagerCharacterInstanceListApplyRenderInterpolation(float fInterpolation) + : m_fInterpolation(fInterpolation) + { + } + + inline void operator () (CInstanceBase* pInstance) + { + pInstance->ApplyRenderInterpolation(m_fInterpolation); + } + + float m_fInterpolation; +}; struct FCharacterManagerCharacterInstanceListDeform { inline void operator () (CInstanceBase * pInstance) @@ -366,6 +394,12 @@ struct FCharacterManagerCharacterInstanceListDeform } }; +void CPythonCharacterManager::ApplyRenderInterpolation(float fInterpolation) +{ + std::for_each(m_kAliveInstMap.begin(), m_kAliveInstMap.end(), FCharacterManagerCharacterInstanceApplyRenderInterpolation(fInterpolation)); + std::for_each(m_kDeadInstList.begin(), m_kDeadInstList.end(), FCharacterManagerCharacterInstanceListApplyRenderInterpolation(fInterpolation)); +} + void CPythonCharacterManager::Deform() { std::for_each(m_kAliveInstMap.begin(), m_kAliveInstMap.end(), FCharacterManagerCharacterInstanceDeform()); diff --git a/src/UserInterface/PythonCharacterManager.h b/src/UserInterface/PythonCharacterManager.h index e1a7c51..d55ddfa 100644 --- a/src/UserInterface/PythonCharacterManager.h +++ b/src/UserInterface/PythonCharacterManager.h @@ -57,6 +57,7 @@ class CPythonCharacterManager : public CSingleton, publ void DestroyDeviceObjects(); void Update(); + void ApplyRenderInterpolation(float fInterpolation); void Deform(); void Render(); void RenderShadowMainInstance(); diff --git a/src/UserInterface/PythonSystem.cpp b/src/UserInterface/PythonSystem.cpp index 18366d8..afc6850 100644 --- a/src/UserInterface/PythonSystem.cpp +++ b/src/UserInterface/PythonSystem.cpp @@ -318,6 +318,8 @@ void CPythonSystem::SetDefaultConfig() m_Config.bAlwaysShowName = DEFAULT_VALUE_ALWAYS_SHOW_NAME; m_Config.bShowDamage = true; m_Config.bShowSalesText = true; + m_Config.iRenderFPS = 60; + m_Config.bShowPerformanceHUD = false; } bool CPythonSystem::IsWindowed() @@ -365,6 +367,26 @@ void CPythonSystem::SetShowSalesTextFlag(int iFlag) m_Config.bShowSalesText = iFlag == 1 ? true : false; } +int CPythonSystem::GetRenderFPS() +{ + return m_Config.iRenderFPS; +} + +void CPythonSystem::SetRenderFPS(int iFPS) +{ + m_Config.iRenderFPS = std::max(0, std::min(iFPS, 500)); +} + +bool CPythonSystem::IsShowPerformanceHUD() +{ + return m_Config.bShowPerformanceHUD; +} + +void CPythonSystem::SetShowPerformanceHUDFlag(int iFlag) +{ + m_Config.bShowPerformanceHUD = iFlag == 1 ? true : false; +} + bool CPythonSystem::IsAutoTiling() { if (m_Config.bSoftwareTiling == 0) @@ -462,6 +484,10 @@ bool CPythonSystem::LoadConfig() m_Config.bShowDamage = atoi(value) == 1 ? true : false; else if (!stricmp(command, "SHOW_SALESTEXT")) m_Config.bShowSalesText = atoi(value) == 1 ? true : false; + else if (!stricmp(command, "RENDER_FPS")) + m_Config.iRenderFPS = std::max(0, std::min(atoi(value), 500)); + else if (!stricmp(command, "SHOW_PERFORMANCE_HUD")) + m_Config.bShowPerformanceHUD = atoi(value) == 1 ? true : false; } if (m_Config.bWindowed) @@ -552,6 +578,8 @@ bool CPythonSystem::SaveConfig() fprintf(fp, "USE_DEFAULT_IME %d\n", m_Config.bUseDefaultIME); fprintf(fp, "SOFTWARE_TILING %d\n", m_Config.bSoftwareTiling); fprintf(fp, "SHADOW_LEVEL %d\n", m_Config.iShadowLevel); + fprintf(fp, "RENDER_FPS %d\n", m_Config.iRenderFPS); + fprintf(fp, "SHOW_PERFORMANCE_HUD %d\n", m_Config.bShowPerformanceHUD); // MR-14: Fog update by Alaric fprintf(fp, "FOG_LEVEL %d\n", m_Config.iFogLevel); // MR-14: -- END OF -- Fog update by Alaric @@ -607,6 +635,9 @@ const CPythonSystem::TWindowStatus & CPythonSystem::GetWindowStatusReference(int void CPythonSystem::ApplyConfig() // 이전 설정과 현재 설정을 비교해서 바뀐 설정을 적용 한다. { + const bool bRenderFPSChanged = m_OldConfig.iRenderFPS != m_Config.iRenderFPS; + const bool bPerformanceHUDChanged = m_OldConfig.bShowPerformanceHUD != m_Config.bShowPerformanceHUD; + if (m_OldConfig.gamma != m_Config.gamma) { float val = 1.0f; @@ -631,6 +662,12 @@ void CPythonSystem::ApplyConfig() // 이전 설정과 현재 설정을 비교해 CPythonApplication::Instance().SetCursorMode(CPythonApplication::CURSOR_MODE_HARDWARE); } + if (bRenderFPSChanged) + CPythonApplication::Instance().SetFPS(m_Config.iRenderFPS); + + if (bPerformanceHUDChanged) + CPythonApplication::Instance().SetPerformanceHUDVisible(m_Config.bShowPerformanceHUD); + m_OldConfig = m_Config; ChangeSystem(); diff --git a/src/UserInterface/PythonSystem.h b/src/UserInterface/PythonSystem.h index dd53ca7..6e6c518 100644 --- a/src/UserInterface/PythonSystem.h +++ b/src/UserInterface/PythonSystem.h @@ -78,6 +78,8 @@ class CPythonSystem : public CSingleton bool bAlwaysShowName; bool bShowDamage; bool bShowSalesText; + int iRenderFPS; + bool bShowPerformanceHUD; } TConfig; public: @@ -157,6 +159,11 @@ class CPythonSystem : public CSingleton void SetFogLevel(unsigned int level); // MR-14: -- END OF -- Fog update by Alaric + int GetRenderFPS(); + void SetRenderFPS(int iFPS); + bool IsShowPerformanceHUD(); + void SetShowPerformanceHUDFlag(int iFlag); + protected: TResolution m_ResolutionList[RESOLUTION_MAX_NUM]; int m_ResolutionCount; @@ -167,4 +174,4 @@ class CPythonSystem : public CSingleton bool m_isInterfaceConfig; PyObject * m_poInterfaceHandler; TWindowStatus m_WindowStatus[WINDOW_MAX_NUM]; -}; \ No newline at end of file +}; diff --git a/src/UserInterface/PythonSystemModule.cpp b/src/UserInterface/PythonSystemModule.cpp index 294d116..260e50d 100644 --- a/src/UserInterface/PythonSystemModule.cpp +++ b/src/UserInterface/PythonSystemModule.cpp @@ -226,6 +226,36 @@ PyObject * systemIsShowSalesText(PyObject * poSelf, PyObject * poArgs) return Py_BuildValue("i", CPythonSystem::Instance().IsShowSalesText()); } +PyObject * systemGetRenderFPS(PyObject * poSelf, PyObject * poArgs) +{ + return Py_BuildValue("i", CPythonSystem::Instance().GetRenderFPS()); +} + +PyObject * systemSetRenderFPS(PyObject * poSelf, PyObject * poArgs) +{ + int iFPS; + if (!PyTuple_GetInteger(poArgs, 0, &iFPS)) + return Py_BuildException(); + + CPythonSystem::Instance().SetRenderFPS(iFPS); + return Py_BuildNone(); +} + +PyObject * systemIsShowPerformanceHUD(PyObject * poSelf, PyObject * poArgs) +{ + return Py_BuildValue("i", CPythonSystem::Instance().IsShowPerformanceHUD()); +} + +PyObject * systemSetShowPerformanceHUDFlag(PyObject * poSelf, PyObject * poArgs) +{ + int iFlag; + if (!PyTuple_GetInteger(poArgs, 0, &iFlag)) + return Py_BuildException(); + + CPythonSystem::Instance().SetShowPerformanceHUDFlag(iFlag); + return Py_BuildNone(); +} + PyObject * systemSetConfig(PyObject * poSelf, PyObject * poArgs) { int res_index; @@ -445,6 +475,11 @@ void initsystem() { "SetShowSalesTextFlag", systemSetShowSalesTextFlag, METH_VARARGS }, { "IsShowSalesText", systemIsShowSalesText, METH_VARARGS }, + { "GetRenderFPS", systemGetRenderFPS, METH_VARARGS }, + { "SetRenderFPS", systemSetRenderFPS, METH_VARARGS }, + { "IsShowPerformanceHUD", systemIsShowPerformanceHUD, METH_VARARGS }, + { "SetShowPerformanceHUDFlag", systemSetShowPerformanceHUDFlag, METH_VARARGS }, + { "GetShadowLevel", systemGetShadowLevel, METH_VARARGS }, { "SetShadowLevel", systemSetShadowLevel, METH_VARARGS },