forked from metin-server/m2dev-client-src
Compare commits
11 Commits
main
...
issue-4-bi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eda856283 | ||
|
|
8a09d2c76a | ||
|
|
bfe52a81f9 | ||
|
|
49e8eac809 | ||
|
|
0b852faf0e | ||
|
|
b353339bd8 | ||
|
|
db7ae1f841 | ||
|
|
2d9beb4793 | ||
|
|
6ff59498d2 | ||
|
|
ba6af8115b | ||
|
|
ef7cdf2809 |
307
docs/anti-cheat-architecture-2026.md
Normal file
307
docs/anti-cheat-architecture-2026.md
Normal file
@@ -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
|
||||
342
docs/high-fps-client-plan.md
Normal file
342
docs/high-fps-client-plan.md
Normal file
@@ -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.
|
||||
147
docs/pack-profile-analysis.md
Normal file
147
docs/pack-profile-analysis.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Pack Profile Analysis
|
||||
|
||||
The client can now emit a runtime pack profiler report into:
|
||||
|
||||
```text
|
||||
log/pack_profile.txt
|
||||
```
|
||||
|
||||
Enable it with either:
|
||||
|
||||
```bash
|
||||
M2PACK_PROFILE=1 ./scripts/run-wine-headless.sh ./build-mingw64-lld/bin
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
./scripts/run-wine-headless.sh ./build-mingw64-lld/bin -- --m2pack-profile
|
||||
```
|
||||
|
||||
## Typical workflow
|
||||
|
||||
Collect two runs with the same scenario:
|
||||
|
||||
1. legacy `.pck` runtime
|
||||
2. `m2p` runtime
|
||||
|
||||
After each run, copy or rename the profiler output so it is not overwritten:
|
||||
|
||||
```bash
|
||||
cp build-mingw64-lld/bin/log/pack_profile.txt logs/pack_profile.pck.txt
|
||||
cp build-mingw64-lld/bin/log/pack_profile.txt logs/pack_profile.m2p.txt
|
||||
```
|
||||
|
||||
Then compare both runs:
|
||||
|
||||
```bash
|
||||
python3 scripts/pack-profile-report.py \
|
||||
pck=logs/pack_profile.pck.txt \
|
||||
m2p=logs/pack_profile.m2p.txt
|
||||
```
|
||||
|
||||
For repeated testing, use the wrapper scripts:
|
||||
|
||||
```bash
|
||||
./scripts/capture-pack-profile.sh \
|
||||
--runtime-root ../m2dev-client \
|
||||
--label pck
|
||||
```
|
||||
|
||||
This stages the runtime into `build-mingw64-lld/bin`, runs the client with
|
||||
`M2PACK_PROFILE=1`, then archives:
|
||||
|
||||
- raw report: `build-mingw64-lld/bin/log/pack-profile-runs/<label>.pack_profile.txt`
|
||||
- parsed summary: `build-mingw64-lld/bin/log/pack-profile-runs/<label>.summary.txt`
|
||||
|
||||
To run a full `pck` vs `m2p` comparison in one go:
|
||||
|
||||
```bash
|
||||
./scripts/compare-pack-profile-runs.sh \
|
||||
--left-label pck \
|
||||
--left-runtime-root /path/to/runtime-pck \
|
||||
--right-label m2p \
|
||||
--right-runtime-root /path/to/runtime-m2p
|
||||
```
|
||||
|
||||
The script captures both runs back-to-back and writes a combined compare report
|
||||
into the same output directory.
|
||||
|
||||
## Real game-flow smoke compare
|
||||
|
||||
Startup-only runs are useful for bootstrap regressions, but they do not show the
|
||||
real hot path once the client reaches `login`, `loading`, and `game`.
|
||||
|
||||
For that case, use the CH99 GM smoke compare wrapper:
|
||||
|
||||
```bash
|
||||
python3 scripts/compare-pack-profile-gm-smoke.py \
|
||||
--left-label pck-only \
|
||||
--left-runtime-root /path/to/runtime-pck \
|
||||
--right-label secure-mixed \
|
||||
--right-runtime-root /path/to/runtime-m2p \
|
||||
--master-key /path/to/master.key \
|
||||
--sign-pubkey /path/to/signing.pub \
|
||||
--account-login admin
|
||||
```
|
||||
|
||||
What it does:
|
||||
|
||||
- copies the built client into a temporary workspace outside the repository
|
||||
- stages each runtime into that workspace
|
||||
- temporarily updates the selected GM account password and map position
|
||||
- auto-logins through the special GM smoke channel (`11991` by default)
|
||||
- enters game, performs one deterministic GM warp, archives `pack_profile.txt`
|
||||
- restores the account password and the character map/position afterward
|
||||
- deletes the temporary workspace unless `--keep-workspaces` is used
|
||||
|
||||
Archived outputs per run:
|
||||
|
||||
- raw report: `<out-dir>/<label>.pack_profile.txt`
|
||||
- parsed summary: `<out-dir>/<label>.summary.txt`
|
||||
- headless trace: `<out-dir>/<label>.headless_gm_teleport_trace.txt`
|
||||
- startup trace when present: `<out-dir>/<label>.startup_trace.txt`
|
||||
|
||||
This flow is the current best approximation of a real client loading path on the
|
||||
Linux-hosted Wine setup because it records phase markers beyond pure startup.
|
||||
|
||||
You can also summarize a single run:
|
||||
|
||||
```bash
|
||||
python3 scripts/pack-profile-report.py logs/pack_profile.m2p.txt
|
||||
```
|
||||
|
||||
## What to read first
|
||||
|
||||
`Packed Load Totals`
|
||||
|
||||
- Best top-level comparison for pack I/O cost in the measured run.
|
||||
- Focus on `delta_ms` and `delta_pct`.
|
||||
|
||||
`Phase Markers`
|
||||
|
||||
- Shows where startup time actually moved.
|
||||
- Useful for deciding whether the gain happened before login, during loading, or mostly in game.
|
||||
|
||||
`Load Time By Phase`
|
||||
|
||||
- Confirms which phase is paying for asset work.
|
||||
- Usually the most important line is `loading`.
|
||||
|
||||
`Loader Stages`
|
||||
|
||||
- Shows whether the cost is mostly in decrypt or zstd.
|
||||
- For `m2p`, expect small manifest overhead and the main costs in `aead_decrypt` and `zstd_decompress`.
|
||||
- For legacy `.pck`, expect `xchacha20_decrypt` and `zstd_decompress`.
|
||||
|
||||
`Top Phase Pack Loads`
|
||||
|
||||
- Useful when the total difference is small, but one or two packs dominate the budget.
|
||||
|
||||
## Decision hints
|
||||
|
||||
If `Packed Load Totals` improve, but `Phase Markers` do not, the bottleneck is probably outside pack loading.
|
||||
|
||||
If `zstd_decompress` dominates both formats, the next lever is compression strategy and pack layout.
|
||||
|
||||
If decrypt dominates, the next lever is reducing decrypt work on the hot path or changing how often the same data is touched.
|
||||
193
scripts/capture-pack-profile.sh
Executable file
193
scripts/capture-pack-profile.sh
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
capture-pack-profile.sh [options] [build-bin-dir] [-- <client args...>]
|
||||
|
||||
Examples:
|
||||
capture-pack-profile.sh --label pck
|
||||
capture-pack-profile.sh --runtime-root ../m2dev-client --label baseline
|
||||
capture-pack-profile.sh ./build-mingw64-lld/bin -- --m2pack-strict-hash 1
|
||||
|
||||
Options:
|
||||
--runtime-root DIR Stage runtime content from DIR before launching
|
||||
--label NAME Output label for the captured report
|
||||
--out-dir DIR Directory for archived raw + summary reports
|
||||
--timeout SEC Overrides M2_TIMEOUT for the headless run
|
||||
--copy-runtime Materialize runtime instead of symlinking it
|
||||
-h, --help Show this help
|
||||
|
||||
Behavior:
|
||||
- Enables M2PACK_PROFILE=1 for the client run
|
||||
- Archives build-bin-dir/log/pack_profile.txt under --out-dir
|
||||
- Writes a parsed summary next to the raw report
|
||||
- Treats timeout exit codes (124/137) as acceptable if the report exists
|
||||
|
||||
Environment overrides:
|
||||
M2_STAGE_RUNTIME_SCRIPT path to stage-linux-runtime.sh
|
||||
M2_HEADLESS_RUN_SCRIPT path to run-wine-headless.sh
|
||||
M2_PACK_PROFILE_PARSER path to pack-profile-report.py
|
||||
EOF
|
||||
}
|
||||
|
||||
sanitize_label() {
|
||||
printf '%s' "$1" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g'
|
||||
}
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(realpath "$script_dir/..")"
|
||||
default_build_bin_dir="$repo_root/build-mingw64-lld/bin"
|
||||
default_runtime_root="$repo_root/../m2dev-client"
|
||||
|
||||
stage_script="${M2_STAGE_RUNTIME_SCRIPT:-$script_dir/stage-linux-runtime.sh}"
|
||||
run_script="${M2_HEADLESS_RUN_SCRIPT:-$script_dir/run-wine-headless.sh}"
|
||||
parser_script="${M2_PACK_PROFILE_PARSER:-$script_dir/pack-profile-report.py}"
|
||||
|
||||
runtime_root=""
|
||||
label=""
|
||||
out_dir=""
|
||||
timeout_seconds="${M2_TIMEOUT:-20}"
|
||||
copy_runtime=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--runtime-root)
|
||||
runtime_root="$2"
|
||||
shift 2
|
||||
;;
|
||||
--label)
|
||||
label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--out-dir)
|
||||
out_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--timeout)
|
||||
timeout_seconds="$2"
|
||||
shift 2
|
||||
;;
|
||||
--copy-runtime)
|
||||
copy_runtime=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
build_bin_dir="${1:-$default_build_bin_dir}"
|
||||
if [[ $# -gt 0 ]]; then
|
||||
shift
|
||||
fi
|
||||
|
||||
build_bin_dir="$(realpath -m "$build_bin_dir")"
|
||||
|
||||
if [[ -z "$label" ]]; then
|
||||
label="$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
|
||||
safe_label="$(sanitize_label "$label")"
|
||||
if [[ -z "$safe_label" ]]; then
|
||||
safe_label="capture"
|
||||
fi
|
||||
|
||||
if [[ -z "$out_dir" ]]; then
|
||||
out_dir="$build_bin_dir/log/pack-profile-runs"
|
||||
fi
|
||||
out_dir="$(realpath -m "$out_dir")"
|
||||
|
||||
if [[ -z "$runtime_root" ]]; then
|
||||
if [[ ! -e "$build_bin_dir/config/locale.cfg" || ! -e "$build_bin_dir/pack" ]]; then
|
||||
if [[ -d "$default_runtime_root" ]]; then
|
||||
runtime_root="$default_runtime_root"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$runtime_root" ]]; then
|
||||
runtime_root="$(realpath "$runtime_root")"
|
||||
fi
|
||||
|
||||
if [[ ! -x "$run_script" ]]; then
|
||||
echo "headless runner not executable: $run_script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$parser_script" ]]; then
|
||||
echo "pack profile parser not found: $parser_script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "python3 not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$build_bin_dir/log" "$out_dir"
|
||||
|
||||
if [[ -n "$runtime_root" ]]; then
|
||||
if [[ ! -x "$stage_script" ]]; then
|
||||
echo "runtime staging script not executable: $stage_script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stage_args=()
|
||||
if [[ "$copy_runtime" -eq 1 ]]; then
|
||||
stage_args+=(--copy)
|
||||
fi
|
||||
stage_args+=("$runtime_root" "$build_bin_dir")
|
||||
"$stage_script" "${stage_args[@]}"
|
||||
fi
|
||||
|
||||
report_path="$build_bin_dir/log/pack_profile.txt"
|
||||
raw_out="$out_dir/$safe_label.pack_profile.txt"
|
||||
summary_out="$out_dir/$safe_label.summary.txt"
|
||||
|
||||
rm -f "$report_path"
|
||||
|
||||
set +e
|
||||
M2PACK_PROFILE=1 M2_TIMEOUT="$timeout_seconds" "$run_script" "$build_bin_dir" -- "$@"
|
||||
run_exit=$?
|
||||
set -e
|
||||
|
||||
if [[ ! -f "$report_path" ]]; then
|
||||
echo "pack profile report was not generated: $report_path" >&2
|
||||
exit "$run_exit"
|
||||
fi
|
||||
|
||||
cp "$report_path" "$raw_out"
|
||||
python3 "$parser_script" "$raw_out" > "$summary_out"
|
||||
|
||||
case "$run_exit" in
|
||||
0|124|137)
|
||||
;;
|
||||
*)
|
||||
echo "client run exited with $run_exit; archived report anyway:" >&2
|
||||
echo " raw: $raw_out" >&2
|
||||
echo " summary: $summary_out" >&2
|
||||
exit "$run_exit"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "captured pack profile:"
|
||||
echo " raw: $raw_out"
|
||||
echo " summary: $summary_out"
|
||||
728
scripts/compare-pack-profile-gm-smoke.py
Executable file
728
scripts/compare-pack-profile-gm-smoke.py
Executable file
@@ -0,0 +1,728 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_SERVER_HOST = "173.249.9.66"
|
||||
DEFAULT_AUTH_PORT = 11000
|
||||
DEFAULT_CHANNEL_PORT = 11991
|
||||
DEFAULT_MAP_NAME = "gm_guild_build"
|
||||
DEFAULT_MAP_INDEX = 200
|
||||
DEFAULT_GLOBAL_X = 96000
|
||||
DEFAULT_GLOBAL_Y = 12800
|
||||
ACCEPTABLE_RUN_CODES = {0, 124, 137}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerState:
|
||||
player_id: int
|
||||
name: str
|
||||
map_index: int
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountState:
|
||||
account_id: int
|
||||
password_hash: str
|
||||
player: PlayerState
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Run a real login -> game -> GM warp pack profile comparison under "
|
||||
"Wine using two runtime roots."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--left-runtime-root",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Runtime root for the first capture.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--right-runtime-root",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Runtime root for the second capture.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--left-label",
|
||||
default="left",
|
||||
help="Label for the first capture.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--right-label",
|
||||
default="right",
|
||||
help="Label for the second capture.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--master-key",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to the runtime master key hex file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sign-pubkey",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to the signing public key hex file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key-id",
|
||||
default="1",
|
||||
help="Runtime key_id for the secure pack loader.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--account-login",
|
||||
default="admin",
|
||||
help="GM account used for the smoke flow.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slot",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Character slot used for auto-select.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=90,
|
||||
help="Headless client timeout in seconds per capture.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--server-host",
|
||||
default=DEFAULT_SERVER_HOST,
|
||||
help="Server host used in loginInfo.py.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--auth-port",
|
||||
type=int,
|
||||
default=DEFAULT_AUTH_PORT,
|
||||
help="Auth port used in loginInfo.py.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel-port",
|
||||
type=int,
|
||||
default=DEFAULT_CHANNEL_PORT,
|
||||
help="Channel port used in loginInfo.py.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--map-name",
|
||||
default=DEFAULT_MAP_NAME,
|
||||
help="Headless GM target map name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--map-index",
|
||||
type=int,
|
||||
default=DEFAULT_MAP_INDEX,
|
||||
help="Server-side map index used to place the GM character before login.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--global-x",
|
||||
type=int,
|
||||
default=DEFAULT_GLOBAL_X,
|
||||
help="Global X coordinate used for the initial load and GM warp target.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--global-y",
|
||||
type=int,
|
||||
default=DEFAULT_GLOBAL_Y,
|
||||
help="Global Y coordinate used for the initial load and GM warp target.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--command-delay",
|
||||
type=float,
|
||||
default=3.0,
|
||||
help="Delay before the headless GM sends the first /warp command.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--settle-delay",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Delay between warp arrival and the next GM command.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--warp-timeout",
|
||||
type=float,
|
||||
default=15.0,
|
||||
help="Timeout per GM warp step in seconds.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory for archived reports and compare output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--work-root",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory where temporary workspaces are created.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-workspaces",
|
||||
action="store_true",
|
||||
help="Keep temporary workspaces for debugging instead of deleting them.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"source_build_bin_dir",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Source build/bin directory to copy out of the repository.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def sanitize_label(value: str) -> str:
|
||||
safe = []
|
||||
prev_dash = False
|
||||
for ch in value.lower():
|
||||
keep = ch.isalnum() or ch in "._-"
|
||||
if keep:
|
||||
if ch == "-" and prev_dash:
|
||||
continue
|
||||
safe.append(ch)
|
||||
prev_dash = ch == "-"
|
||||
else:
|
||||
if not prev_dash:
|
||||
safe.append("-")
|
||||
prev_dash = True
|
||||
result = "".join(safe).strip("-")
|
||||
return result or "capture"
|
||||
|
||||
|
||||
def require_tool(name: str) -> None:
|
||||
if shutil.which(name) is None:
|
||||
raise SystemExit(f"{name} not found in PATH")
|
||||
|
||||
|
||||
def run_checked(args: list[str], *, cwd: Path | None = None, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
|
||||
completed = subprocess.run(
|
||||
args,
|
||||
cwd=str(cwd) if cwd else None,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise RuntimeError(f"command failed ({completed.returncode}): {' '.join(args)}\n{detail}")
|
||||
return completed
|
||||
|
||||
|
||||
def run_mysql(database: str, sql: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["mysql", "-B", "-N", database, "-e", sql],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise RuntimeError(f"mysql failed: {detail}")
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def sql_quote(value: str) -> str:
|
||||
return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
||||
|
||||
|
||||
def load_account_state(account_login: str, slot: int) -> AccountState:
|
||||
if slot < 0 or slot > 3:
|
||||
raise SystemExit(f"unsupported slot index: {slot}")
|
||||
|
||||
account_id_text = run_mysql(
|
||||
"account",
|
||||
f"SELECT id FROM account WHERE login={sql_quote(account_login)} LIMIT 1;",
|
||||
)
|
||||
if not account_id_text:
|
||||
raise SystemExit(f"account not found: {account_login}")
|
||||
account_id = int(account_id_text)
|
||||
|
||||
password_hash = run_mysql(
|
||||
"account",
|
||||
f"SELECT password FROM account WHERE login={sql_quote(account_login)} LIMIT 1;",
|
||||
)
|
||||
if not password_hash:
|
||||
raise SystemExit(f"account password hash not found: {account_login}")
|
||||
|
||||
pid_text = run_mysql(
|
||||
"player",
|
||||
f"SELECT pid{slot + 1} FROM player_index WHERE id={account_id} LIMIT 1;",
|
||||
)
|
||||
if not pid_text or pid_text == "0":
|
||||
raise SystemExit(f"slot {slot} has no character for account: {account_login}")
|
||||
player_id = int(pid_text)
|
||||
|
||||
player_row = run_mysql(
|
||||
"player",
|
||||
f"SELECT id, name, map_index, x, y FROM player WHERE id={player_id} LIMIT 1;",
|
||||
)
|
||||
if not player_row:
|
||||
raise SystemExit(f"player row not found for id={player_id}")
|
||||
parts = player_row.split("\t")
|
||||
if len(parts) != 5:
|
||||
raise SystemExit(f"unexpected player row format for id={player_id}: {player_row}")
|
||||
|
||||
return AccountState(
|
||||
account_id=account_id,
|
||||
password_hash=password_hash,
|
||||
player=PlayerState(
|
||||
player_id=int(parts[0]),
|
||||
name=parts[1],
|
||||
map_index=int(parts[2]),
|
||||
x=int(parts[3]),
|
||||
y=int(parts[4]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def restore_account_state(account_login: str, state: AccountState) -> None:
|
||||
run_mysql(
|
||||
"account",
|
||||
(
|
||||
"UPDATE account SET password=%s WHERE login=%s;"
|
||||
% (sql_quote(state.password_hash), sql_quote(account_login))
|
||||
),
|
||||
)
|
||||
run_mysql(
|
||||
"player",
|
||||
(
|
||||
"UPDATE player SET map_index=%d, x=%d, y=%d WHERE id=%d;"
|
||||
% (
|
||||
state.player.map_index,
|
||||
state.player.x,
|
||||
state.player.y,
|
||||
state.player.player_id,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def verify_account_state(account_login: str, slot: int, expected: AccountState) -> None:
|
||||
current = load_account_state(account_login, slot)
|
||||
if current.password_hash != expected.password_hash:
|
||||
raise RuntimeError(
|
||||
f"account password hash did not restore for {account_login}"
|
||||
)
|
||||
if (
|
||||
current.player.map_index != expected.player.map_index
|
||||
or current.player.x != expected.player.x
|
||||
or current.player.y != expected.player.y
|
||||
or current.player.player_id != expected.player.player_id
|
||||
):
|
||||
raise RuntimeError(
|
||||
"player state did not restore for "
|
||||
f"{account_login}: expected map={expected.player.map_index} "
|
||||
f"x={expected.player.x} y={expected.player.y}, got "
|
||||
f"map={current.player.map_index} x={current.player.x} y={current.player.y}"
|
||||
)
|
||||
|
||||
|
||||
def set_temp_account_state(
|
||||
account_login: str,
|
||||
player_id: int,
|
||||
*,
|
||||
temp_password: str,
|
||||
map_index: int,
|
||||
global_x: int,
|
||||
global_y: int,
|
||||
) -> None:
|
||||
run_mysql(
|
||||
"account",
|
||||
(
|
||||
"UPDATE account SET password=PASSWORD(%s) WHERE login=%s;"
|
||||
% (sql_quote(temp_password), sql_quote(account_login))
|
||||
),
|
||||
)
|
||||
run_mysql(
|
||||
"player",
|
||||
(
|
||||
"UPDATE player SET map_index=%d, x=%d, y=%d WHERE id=%d;"
|
||||
% (map_index, global_x, global_y, player_id)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def remove_previous_logs(build_bin_dir: Path) -> None:
|
||||
log_dir = build_bin_dir / "log"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
for path in log_dir.glob("*.txt"):
|
||||
path.unlink(missing_ok=True)
|
||||
for path in [build_bin_dir / "syserr.txt", build_bin_dir / "ErrorLog.txt", build_bin_dir / "loginInfo.py"]:
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def write_login_info(
|
||||
build_bin_dir: Path,
|
||||
*,
|
||||
account_login: str,
|
||||
temp_password: str,
|
||||
slot: int,
|
||||
server_host: str,
|
||||
auth_port: int,
|
||||
channel_port: int,
|
||||
) -> None:
|
||||
login_info = "\n".join(
|
||||
[
|
||||
f"addr={server_host!r}",
|
||||
f"port={channel_port}",
|
||||
f"account_addr={server_host!r}",
|
||||
f"account_port={auth_port}",
|
||||
f"id={account_login!r}",
|
||||
f"pwd={temp_password!r}",
|
||||
f"slot={slot}",
|
||||
"autoLogin=1",
|
||||
"autoSelect=1",
|
||||
"",
|
||||
]
|
||||
)
|
||||
(build_bin_dir / "loginInfo.py").write_text(login_info, encoding="utf-8")
|
||||
|
||||
|
||||
def copy_source_bin(source_build_bin_dir: Path, workspace_bin_dir: Path) -> None:
|
||||
run_checked(
|
||||
[
|
||||
"rsync",
|
||||
"-a",
|
||||
"--delete",
|
||||
"--exclude",
|
||||
"config",
|
||||
"--exclude",
|
||||
"pack",
|
||||
"--exclude",
|
||||
"mark",
|
||||
"--exclude",
|
||||
"log",
|
||||
"--exclude",
|
||||
"BGM",
|
||||
"--exclude",
|
||||
"bgm",
|
||||
"--exclude",
|
||||
"locale",
|
||||
"--exclude",
|
||||
"sound",
|
||||
f"{source_build_bin_dir}/",
|
||||
str(workspace_bin_dir),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def copy_if_exists(src: Path, dst: Path) -> None:
|
||||
if src.exists():
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
|
||||
def prepare_runtime_wrapper(runtime_root: Path, wrapper_root: Path) -> None:
|
||||
if not (runtime_root / "config").exists():
|
||||
raise SystemExit(f"runtime config missing: {runtime_root / 'config'}")
|
||||
if not (runtime_root / "pack").exists():
|
||||
raise SystemExit(f"runtime pack missing: {runtime_root / 'pack'}")
|
||||
|
||||
wrapper_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def link_entry(name: str, target: Path) -> None:
|
||||
link_path = wrapper_root / name
|
||||
if link_path.exists() or link_path.is_symlink():
|
||||
link_path.unlink()
|
||||
os.symlink(target, link_path)
|
||||
|
||||
link_entry("config", runtime_root / "config")
|
||||
link_entry("pack", runtime_root / "pack")
|
||||
|
||||
optional_entries = {
|
||||
"mark": runtime_root / "mark",
|
||||
"locale": runtime_root / "locale",
|
||||
"sound": runtime_root / "sound",
|
||||
}
|
||||
|
||||
bgm_target = runtime_root / "BGM"
|
||||
if not bgm_target.exists():
|
||||
bgm_target = runtime_root / "bgm"
|
||||
if bgm_target.exists():
|
||||
optional_entries["BGM"] = bgm_target
|
||||
|
||||
for name, target in optional_entries.items():
|
||||
if target.exists():
|
||||
link_entry(name, target)
|
||||
|
||||
|
||||
def capture_run(
|
||||
*,
|
||||
label: str,
|
||||
runtime_root: Path,
|
||||
source_build_bin_dir: Path,
|
||||
out_dir: Path,
|
||||
work_root: Path | None,
|
||||
keep_workspace: bool,
|
||||
master_key_hex: str,
|
||||
sign_pubkey_hex: str,
|
||||
key_id: str,
|
||||
account_login: str,
|
||||
slot: int,
|
||||
timeout_seconds: int,
|
||||
server_host: str,
|
||||
auth_port: int,
|
||||
channel_port: int,
|
||||
map_name: str,
|
||||
map_index: int,
|
||||
global_x: int,
|
||||
global_y: int,
|
||||
command_delay: float,
|
||||
settle_delay: float,
|
||||
warp_timeout: float,
|
||||
stage_script: Path,
|
||||
run_script: Path,
|
||||
parser_script: Path,
|
||||
) -> tuple[Path, Path | None]:
|
||||
workspace_path = Path(
|
||||
tempfile.mkdtemp(
|
||||
prefix=f"pack-profile-gm-smoke-{sanitize_label(label)}.",
|
||||
dir=str(work_root) if work_root else None,
|
||||
)
|
||||
)
|
||||
workspace_bin_dir = workspace_path / "bin"
|
||||
workspace_runtime_dir = workspace_path / "runtime"
|
||||
workspace_bin_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
account_state = load_account_state(account_login, slot)
|
||||
temp_password = ("Tmp" + secrets.token_hex(6))[:16]
|
||||
safe_label = sanitize_label(label)
|
||||
raw_out = out_dir / f"{safe_label}.pack_profile.txt"
|
||||
summary_out = out_dir / f"{safe_label}.summary.txt"
|
||||
trace_out = out_dir / f"{safe_label}.headless_gm_teleport_trace.txt"
|
||||
startup_out = out_dir / f"{safe_label}.startup_trace.txt"
|
||||
syserr_out = out_dir / f"{safe_label}.syserr.txt"
|
||||
|
||||
try:
|
||||
copy_source_bin(source_build_bin_dir, workspace_bin_dir)
|
||||
remove_previous_logs(workspace_bin_dir)
|
||||
prepare_runtime_wrapper(runtime_root, workspace_runtime_dir)
|
||||
run_checked(["bash", str(stage_script), str(workspace_runtime_dir), str(workspace_bin_dir)])
|
||||
write_login_info(
|
||||
workspace_bin_dir,
|
||||
account_login=account_login,
|
||||
temp_password=temp_password,
|
||||
slot=slot,
|
||||
server_host=server_host,
|
||||
auth_port=auth_port,
|
||||
channel_port=channel_port,
|
||||
)
|
||||
set_temp_account_state(
|
||||
account_login,
|
||||
account_state.player.player_id,
|
||||
temp_password=temp_password,
|
||||
map_index=map_index,
|
||||
global_x=global_x,
|
||||
global_y=global_y,
|
||||
)
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"M2PACK_PROFILE": "1",
|
||||
"M2PACK_MASTER_KEY_HEX": master_key_hex,
|
||||
"M2PACK_SIGN_PUBKEY_HEX": sign_pubkey_hex,
|
||||
"M2PACK_KEY_ID": key_id,
|
||||
"M2_TIMEOUT": str(timeout_seconds),
|
||||
"M2_HEADLESS_SCENARIO": "gm_teleport",
|
||||
"M2_HEADLESS_WARP_STEPS": f"{map_name},{global_x},{global_y}",
|
||||
"M2_HEADLESS_COMMAND_DELAY": str(command_delay),
|
||||
"M2_HEADLESS_SETTLE_DELAY": str(settle_delay),
|
||||
"M2_HEADLESS_WARP_TIMEOUT": str(warp_timeout),
|
||||
}
|
||||
)
|
||||
env.setdefault("WINEDEBUG", "-all")
|
||||
|
||||
completed = subprocess.run(
|
||||
["bash", str(run_script), str(workspace_bin_dir)],
|
||||
cwd=str(workspace_bin_dir),
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
report_path = workspace_bin_dir / "log" / "pack_profile.txt"
|
||||
if not report_path.is_file():
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise RuntimeError(f"pack profile report not generated for {label}: {detail}")
|
||||
|
||||
trace_path = workspace_bin_dir / "log" / "headless_gm_teleport_trace.txt"
|
||||
startup_path = workspace_bin_dir / "log" / "startup_trace.txt"
|
||||
syserr_path = workspace_bin_dir / "syserr.txt"
|
||||
|
||||
shutil.copy2(report_path, raw_out)
|
||||
copy_if_exists(trace_path, trace_out)
|
||||
copy_if_exists(startup_path, startup_out)
|
||||
copy_if_exists(syserr_path, syserr_out)
|
||||
|
||||
summary = run_checked([sys.executable, str(parser_script), str(raw_out)])
|
||||
summary_out.write_text(summary.stdout, encoding="utf-8")
|
||||
|
||||
trace_text = trace_path.read_text(encoding="utf-8", errors="ignore") if trace_path.exists() else ""
|
||||
scenario_ok = "Scenario success current_map=" in trace_text
|
||||
|
||||
if completed.returncode not in ACCEPTABLE_RUN_CODES:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip()
|
||||
raise RuntimeError(
|
||||
f"client run for {label} exited with {completed.returncode}: {detail}"
|
||||
)
|
||||
|
||||
if not scenario_ok:
|
||||
raise RuntimeError(
|
||||
f"GM smoke scenario for {label} did not finish successfully; "
|
||||
f"see {trace_out if trace_out.exists() else trace_path}"
|
||||
)
|
||||
|
||||
return raw_out, (workspace_path if keep_workspace else None)
|
||||
finally:
|
||||
try:
|
||||
restore_account_state(account_login, account_state)
|
||||
verify_account_state(account_login, slot, account_state)
|
||||
finally:
|
||||
if not keep_workspace:
|
||||
shutil.rmtree(workspace_path, ignore_errors=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
require_tool("mysql")
|
||||
require_tool("rsync")
|
||||
require_tool("python3")
|
||||
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
repo_root = script_dir.parent
|
||||
source_build_bin_dir = (
|
||||
Path(args.source_build_bin_dir).resolve()
|
||||
if args.source_build_bin_dir
|
||||
else (repo_root / "build-mingw64-lld" / "bin").resolve()
|
||||
)
|
||||
if not (source_build_bin_dir / "Metin2_RelWithDebInfo.exe").is_file():
|
||||
raise SystemExit(f"client exe not found: {source_build_bin_dir / 'Metin2_RelWithDebInfo.exe'}")
|
||||
|
||||
stage_script = script_dir / "stage-linux-runtime.sh"
|
||||
run_script = script_dir / "run-wine-headless.sh"
|
||||
parser_script = script_dir / "pack-profile-report.py"
|
||||
|
||||
for path, kind in [
|
||||
(stage_script, "runtime stage script"),
|
||||
(run_script, "headless run script"),
|
||||
(parser_script, "pack profile parser"),
|
||||
]:
|
||||
if not path.exists():
|
||||
raise SystemExit(f"{kind} not found: {path}")
|
||||
|
||||
out_dir = (
|
||||
args.out_dir.resolve()
|
||||
if args.out_dir
|
||||
else (source_build_bin_dir / "log" / "pack-profile-gm-smoke" / next(tempfile._get_candidate_names())).resolve()
|
||||
)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
work_root = args.work_root.resolve() if args.work_root else None
|
||||
if work_root:
|
||||
work_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
master_key_hex = args.master_key.read_text(encoding="utf-8").strip()
|
||||
sign_pubkey_hex = args.sign_pubkey.read_text(encoding="utf-8").strip()
|
||||
if not master_key_hex:
|
||||
raise SystemExit(f"master key file is empty: {args.master_key}")
|
||||
if not sign_pubkey_hex:
|
||||
raise SystemExit(f"sign pubkey file is empty: {args.sign_pubkey}")
|
||||
|
||||
left_report, left_workspace = capture_run(
|
||||
label=args.left_label,
|
||||
runtime_root=args.left_runtime_root.resolve(),
|
||||
source_build_bin_dir=source_build_bin_dir,
|
||||
out_dir=out_dir,
|
||||
work_root=work_root,
|
||||
keep_workspace=args.keep_workspaces,
|
||||
master_key_hex=master_key_hex,
|
||||
sign_pubkey_hex=sign_pubkey_hex,
|
||||
key_id=args.key_id,
|
||||
account_login=args.account_login,
|
||||
slot=args.slot,
|
||||
timeout_seconds=args.timeout,
|
||||
server_host=args.server_host,
|
||||
auth_port=args.auth_port,
|
||||
channel_port=args.channel_port,
|
||||
map_name=args.map_name,
|
||||
map_index=args.map_index,
|
||||
global_x=args.global_x,
|
||||
global_y=args.global_y,
|
||||
command_delay=args.command_delay,
|
||||
settle_delay=args.settle_delay,
|
||||
warp_timeout=args.warp_timeout,
|
||||
stage_script=stage_script,
|
||||
run_script=run_script,
|
||||
parser_script=parser_script,
|
||||
)
|
||||
|
||||
right_report, right_workspace = capture_run(
|
||||
label=args.right_label,
|
||||
runtime_root=args.right_runtime_root.resolve(),
|
||||
source_build_bin_dir=source_build_bin_dir,
|
||||
out_dir=out_dir,
|
||||
work_root=work_root,
|
||||
keep_workspace=args.keep_workspaces,
|
||||
master_key_hex=master_key_hex,
|
||||
sign_pubkey_hex=sign_pubkey_hex,
|
||||
key_id=args.key_id,
|
||||
account_login=args.account_login,
|
||||
slot=args.slot,
|
||||
timeout_seconds=args.timeout,
|
||||
server_host=args.server_host,
|
||||
auth_port=args.auth_port,
|
||||
channel_port=args.channel_port,
|
||||
map_name=args.map_name,
|
||||
map_index=args.map_index,
|
||||
global_x=args.global_x,
|
||||
global_y=args.global_y,
|
||||
command_delay=args.command_delay,
|
||||
settle_delay=args.settle_delay,
|
||||
warp_timeout=args.warp_timeout,
|
||||
stage_script=stage_script,
|
||||
run_script=run_script,
|
||||
parser_script=parser_script,
|
||||
)
|
||||
|
||||
compare = run_checked(
|
||||
[
|
||||
sys.executable,
|
||||
str(parser_script),
|
||||
f"{args.left_label}={left_report}",
|
||||
f"{args.right_label}={right_report}",
|
||||
]
|
||||
)
|
||||
|
||||
compare_path = out_dir / (
|
||||
f"compare-{sanitize_label(args.left_label)}-vs-{sanitize_label(args.right_label)}.txt"
|
||||
)
|
||||
compare_path.write_text(compare.stdout, encoding="utf-8")
|
||||
print(compare.stdout, end="")
|
||||
print()
|
||||
print(f"saved compare report: {compare_path}")
|
||||
if left_workspace:
|
||||
print(f"kept workspace: {left_workspace}")
|
||||
if right_workspace:
|
||||
print(f"kept workspace: {right_workspace}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
176
scripts/compare-pack-profile-runs.sh
Executable file
176
scripts/compare-pack-profile-runs.sh
Executable file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
compare-pack-profile-runs.sh [options] [build-bin-dir] [-- <client args...>]
|
||||
|
||||
Examples:
|
||||
compare-pack-profile-runs.sh \
|
||||
--left-label pck \
|
||||
--left-runtime-root /srv/client-runtime-pck \
|
||||
--right-label m2p \
|
||||
--right-runtime-root /srv/client-runtime-m2p
|
||||
|
||||
compare-pack-profile-runs.sh \
|
||||
--left-label legacy \
|
||||
--left-runtime-root ../runtime-pck \
|
||||
--right-label secure \
|
||||
--right-runtime-root ../runtime-m2p \
|
||||
./build-mingw64-lld/bin -- --m2pack-strict-hash 0
|
||||
|
||||
Options:
|
||||
--left-runtime-root DIR Runtime root for the first run
|
||||
--right-runtime-root DIR Runtime root for the second run
|
||||
--left-label NAME Label for the first run (default: left)
|
||||
--right-label NAME Label for the second run (default: right)
|
||||
--out-dir DIR Directory for archived reports and compare output
|
||||
--timeout SEC Overrides M2_TIMEOUT for both runs
|
||||
--copy-runtime Materialize runtime instead of symlinking it
|
||||
-h, --help Show this help
|
||||
|
||||
Behavior:
|
||||
- Runs two captures back-to-back into the same build output
|
||||
- Archives both raw reports and summaries
|
||||
- Writes a combined compare report and prints it to stdout
|
||||
EOF
|
||||
}
|
||||
|
||||
sanitize_label() {
|
||||
printf '%s' "$1" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g'
|
||||
}
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(realpath "$script_dir/..")"
|
||||
default_build_bin_dir="$repo_root/build-mingw64-lld/bin"
|
||||
|
||||
capture_script="${M2_CAPTURE_PACK_PROFILE_SCRIPT:-$script_dir/capture-pack-profile.sh}"
|
||||
parser_script="${M2_PACK_PROFILE_PARSER:-$script_dir/pack-profile-report.py}"
|
||||
|
||||
left_runtime_root=""
|
||||
right_runtime_root=""
|
||||
left_label="left"
|
||||
right_label="right"
|
||||
out_dir=""
|
||||
timeout_seconds="${M2_TIMEOUT:-20}"
|
||||
copy_runtime=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--left-runtime-root)
|
||||
left_runtime_root="$2"
|
||||
shift 2
|
||||
;;
|
||||
--right-runtime-root)
|
||||
right_runtime_root="$2"
|
||||
shift 2
|
||||
;;
|
||||
--left-label)
|
||||
left_label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--right-label)
|
||||
right_label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--out-dir)
|
||||
out_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--timeout)
|
||||
timeout_seconds="$2"
|
||||
shift 2
|
||||
;;
|
||||
--copy-runtime)
|
||||
copy_runtime=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$left_runtime_root" || -z "$right_runtime_root" ]]; then
|
||||
echo "both --left-runtime-root and --right-runtime-root are required" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_bin_dir="${1:-$default_build_bin_dir}"
|
||||
if [[ $# -gt 0 ]]; then
|
||||
shift
|
||||
fi
|
||||
build_bin_dir="$(realpath -m "$build_bin_dir")"
|
||||
|
||||
if [[ -z "$out_dir" ]]; then
|
||||
out_dir="$build_bin_dir/log/pack-profile-runs/$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
out_dir="$(realpath -m "$out_dir")"
|
||||
mkdir -p "$out_dir"
|
||||
|
||||
if [[ ! -x "$capture_script" ]]; then
|
||||
echo "capture script not executable: $capture_script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$parser_script" ]]; then
|
||||
echo "pack profile parser not found: $parser_script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "python3 not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
capture_common_args=(
|
||||
--out-dir "$out_dir"
|
||||
--timeout "$timeout_seconds"
|
||||
)
|
||||
if [[ "$copy_runtime" -eq 1 ]]; then
|
||||
capture_common_args+=(--copy-runtime)
|
||||
fi
|
||||
|
||||
"$capture_script" \
|
||||
"${capture_common_args[@]}" \
|
||||
--runtime-root "$left_runtime_root" \
|
||||
--label "$left_label" \
|
||||
"$build_bin_dir" \
|
||||
-- "$@"
|
||||
|
||||
"$capture_script" \
|
||||
"${capture_common_args[@]}" \
|
||||
--runtime-root "$right_runtime_root" \
|
||||
--label "$right_label" \
|
||||
"$build_bin_dir" \
|
||||
-- "$@"
|
||||
|
||||
left_safe_label="$(sanitize_label "$left_label")"
|
||||
right_safe_label="$(sanitize_label "$right_label")"
|
||||
left_report="$out_dir/$left_safe_label.pack_profile.txt"
|
||||
right_report="$out_dir/$right_safe_label.pack_profile.txt"
|
||||
compare_path="$out_dir/compare-$left_safe_label-vs-$right_safe_label.txt"
|
||||
|
||||
python3 "$parser_script" \
|
||||
"$left_label=$left_report" \
|
||||
"$right_label=$right_report" | tee "$compare_path"
|
||||
|
||||
echo
|
||||
echo "saved compare report: $compare_path"
|
||||
755
scripts/pack-profile-report.py
Executable file
755
scripts/pack-profile-report.py
Executable file
@@ -0,0 +1,755 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
TABLE_SECTIONS = {
|
||||
"mounts_by_format",
|
||||
"mounts_by_pack",
|
||||
"loads_by_format",
|
||||
"loads_by_phase_format",
|
||||
"top_phase_pack_loads",
|
||||
"top_extension_loads",
|
||||
"loader_stages",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Report:
|
||||
label: str
|
||||
path: Path
|
||||
metadata: dict[str, str] = field(default_factory=dict)
|
||||
phase_markers: list[tuple[str, float]] = field(default_factory=list)
|
||||
tables: dict[str, dict[str, dict[str, float]]] = field(default_factory=dict)
|
||||
|
||||
def uptime_ms(self) -> float:
|
||||
return parse_float(self.metadata.get("uptime_ms", "0"))
|
||||
|
||||
def current_phase(self) -> str:
|
||||
return self.metadata.get("current_phase", "-")
|
||||
|
||||
def reason(self) -> str:
|
||||
return self.metadata.get("reason", "-")
|
||||
|
||||
def section(self, name: str) -> dict[str, dict[str, float]]:
|
||||
return self.tables.get(name, {})
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Summarize or compare pack_profile.txt reports generated by the "
|
||||
"client-side M2 pack profiler."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"reports",
|
||||
nargs="+",
|
||||
metavar="REPORT",
|
||||
help=(
|
||||
"Either /path/to/pack_profile.txt or label=/path/to/pack_profile.txt. "
|
||||
"Pass one report for a summary, or two or more reports for a comparison."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top",
|
||||
type=int,
|
||||
default=6,
|
||||
help="How many hotspot rows to show in top-pack, top-extension and stage sections.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_float(value: str) -> float:
|
||||
try:
|
||||
return float(value.strip())
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def parse_metric_value(value: str) -> float:
|
||||
value = value.strip()
|
||||
if value.endswith(" ms"):
|
||||
value = value[:-3]
|
||||
return parse_float(value)
|
||||
|
||||
|
||||
def parse_report_arg(value: str) -> tuple[str, Path]:
|
||||
if "=" in value:
|
||||
label, path_value = value.split("=", 1)
|
||||
if label and path_value:
|
||||
return label, Path(path_value)
|
||||
|
||||
path = Path(value)
|
||||
return path.stem, path
|
||||
|
||||
|
||||
def load_report(arg_value: str) -> Report:
|
||||
label, path = parse_report_arg(arg_value)
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"report not found: {path}")
|
||||
|
||||
report = Report(label=label, path=path)
|
||||
current_section: str | None = None
|
||||
|
||||
for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line == "PACK PROFILE REPORT":
|
||||
continue
|
||||
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
current_section = line[1:-1]
|
||||
if current_section in TABLE_SECTIONS:
|
||||
report.tables.setdefault(current_section, {})
|
||||
continue
|
||||
|
||||
if current_section is None:
|
||||
if "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
report.metadata[key.strip()] = value.strip()
|
||||
continue
|
||||
|
||||
if current_section == "phase_markers":
|
||||
if "\t" not in line:
|
||||
continue
|
||||
phase, value = line.split("\t", 1)
|
||||
report.phase_markers.append((phase.strip(), parse_metric_value(value)))
|
||||
continue
|
||||
|
||||
if current_section not in TABLE_SECTIONS:
|
||||
continue
|
||||
|
||||
fields = [field.strip() for field in line.split("\t") if field.strip()]
|
||||
if not fields:
|
||||
continue
|
||||
|
||||
key = fields[0]
|
||||
metrics: dict[str, float] = {}
|
||||
for field in fields[1:]:
|
||||
if "=" not in field:
|
||||
continue
|
||||
metric_key, metric_value = field.split("=", 1)
|
||||
metrics[metric_key.strip()] = parse_metric_value(metric_value)
|
||||
|
||||
report.tables[current_section][key] = metrics
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def aggregate_phase_loads(report: Report) -> dict[str, dict[str, float]]:
|
||||
aggregated: dict[str, dict[str, float]] = {}
|
||||
for key, metrics in report.section("loads_by_phase_format").items():
|
||||
phase, _, _format = key.partition("|")
|
||||
phase_key = phase.strip()
|
||||
bucket = aggregated.setdefault(
|
||||
phase_key,
|
||||
{"count": 0.0, "fail": 0.0, "bytes": 0.0, "time_ms": 0.0},
|
||||
)
|
||||
for metric_name in ("count", "fail", "bytes", "time_ms"):
|
||||
bucket[metric_name] += metrics.get(metric_name, 0.0)
|
||||
return aggregated
|
||||
|
||||
|
||||
def sum_section_metrics(section: dict[str, dict[str, float]]) -> dict[str, float]:
|
||||
totals: dict[str, float] = {}
|
||||
for metrics in section.values():
|
||||
for key, value in metrics.items():
|
||||
totals[key] = totals.get(key, 0.0) + value
|
||||
return totals
|
||||
|
||||
|
||||
def sum_selected_section_metrics(
|
||||
section: dict[str, dict[str, float]],
|
||||
include_keys: Iterable[str] | None = None,
|
||||
exclude_keys: Iterable[str] | None = None,
|
||||
) -> dict[str, float]:
|
||||
include_set = set(include_keys or [])
|
||||
exclude_set = set(exclude_keys or [])
|
||||
filtered: dict[str, dict[str, float]] = {}
|
||||
for key, metrics in section.items():
|
||||
if include_set and key not in include_set:
|
||||
continue
|
||||
if key in exclude_set:
|
||||
continue
|
||||
filtered[key] = metrics
|
||||
return sum_section_metrics(filtered)
|
||||
|
||||
|
||||
def sort_rows_by_metric(
|
||||
section: dict[str, dict[str, float]],
|
||||
metric: str,
|
||||
limit: int,
|
||||
) -> list[tuple[str, dict[str, float]]]:
|
||||
rows = list(section.items())
|
||||
rows.sort(key=lambda item: (-item[1].get(metric, 0.0), item[0]))
|
||||
return rows[:limit]
|
||||
|
||||
|
||||
def format_ms(value: float) -> str:
|
||||
return f"{value:.3f}"
|
||||
|
||||
|
||||
def format_delta(value: float) -> str:
|
||||
return f"{value:+.3f}"
|
||||
|
||||
|
||||
def format_percent(value: float | None) -> str:
|
||||
if value is None or math.isinf(value) or math.isnan(value):
|
||||
return "-"
|
||||
return f"{value:+.1f}%"
|
||||
|
||||
|
||||
def format_bytes(value: float) -> str:
|
||||
units = ("B", "KiB", "MiB", "GiB")
|
||||
size = float(value)
|
||||
unit_index = 0
|
||||
while abs(size) >= 1024.0 and unit_index < len(units) - 1:
|
||||
size /= 1024.0
|
||||
unit_index += 1
|
||||
|
||||
if unit_index == 0:
|
||||
return f"{int(round(size))} {units[unit_index]}"
|
||||
return f"{size:.2f} {units[unit_index]}"
|
||||
|
||||
|
||||
def format_count(value: float) -> str:
|
||||
return str(int(round(value)))
|
||||
|
||||
|
||||
def format_metric(metric: str, value: float) -> str:
|
||||
if metric in {"bytes", "in", "out"}:
|
||||
return format_bytes(value)
|
||||
if metric in {"time_ms"}:
|
||||
return format_ms(value)
|
||||
if metric in {"count", "fail", "entries"}:
|
||||
return format_count(value)
|
||||
if metric.endswith("_ms"):
|
||||
return format_ms(value)
|
||||
if metric.endswith("_us"):
|
||||
return f"{value:.1f}"
|
||||
if abs(value - round(value)) < 0.000001:
|
||||
return format_count(value)
|
||||
return f"{value:.3f}"
|
||||
|
||||
|
||||
def render_table(headers: list[str], rows: list[list[str]], numeric_columns: set[int] | None = None) -> str:
|
||||
numeric_columns = numeric_columns or set()
|
||||
widths = [len(header) for header in headers]
|
||||
for row in rows:
|
||||
for index, cell in enumerate(row):
|
||||
widths[index] = max(widths[index], len(cell))
|
||||
|
||||
lines = []
|
||||
header_cells = []
|
||||
for index, header in enumerate(headers):
|
||||
align = str.rjust if index in numeric_columns else str.ljust
|
||||
header_cells.append(align(header, widths[index]))
|
||||
lines.append(" ".join(header_cells))
|
||||
|
||||
divider = []
|
||||
for index, width in enumerate(widths):
|
||||
divider.append(("-" * width))
|
||||
lines.append(" ".join(divider))
|
||||
|
||||
for row in rows:
|
||||
cells = []
|
||||
for index, cell in enumerate(row):
|
||||
align = str.rjust if index in numeric_columns else str.ljust
|
||||
cells.append(align(cell, widths[index]))
|
||||
lines.append(" ".join(cells))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def relative_delta_percent(base: float, candidate: float) -> float | None:
|
||||
if abs(base) < 0.000001:
|
||||
return None
|
||||
return ((candidate - base) / base) * 100.0
|
||||
|
||||
|
||||
def summarize_report(report: Report, top: int) -> str:
|
||||
lines = [
|
||||
f"== {report.label} ==",
|
||||
f"path: {report.path}",
|
||||
(
|
||||
f"reason={report.reason()} phase={report.current_phase()} "
|
||||
f"uptime_ms={format_ms(report.uptime_ms())}"
|
||||
),
|
||||
"",
|
||||
]
|
||||
|
||||
load_totals = sum_section_metrics(report.section("loads_by_format"))
|
||||
mount_totals = sum_section_metrics(report.section("mounts_by_format"))
|
||||
overview_rows = [[
|
||||
format_count(load_totals.get("count", 0.0)),
|
||||
format_ms(load_totals.get("time_ms", 0.0)),
|
||||
format_bytes(load_totals.get("bytes", 0.0)),
|
||||
format_count(load_totals.get("fail", 0.0)),
|
||||
format_count(mount_totals.get("count", 0.0)),
|
||||
format_ms(mount_totals.get("time_ms", 0.0)),
|
||||
]]
|
||||
lines.extend([
|
||||
"Overview",
|
||||
render_table(
|
||||
["loads", "load_ms", "load_bytes", "load_fail", "mounts", "mount_ms"],
|
||||
overview_rows,
|
||||
numeric_columns={0, 1, 2, 3, 4, 5},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
if report.phase_markers:
|
||||
phase_rows = [[phase, format_ms(value)] for phase, value in report.phase_markers]
|
||||
lines.extend([
|
||||
"Phase Markers",
|
||||
render_table(["phase", "ms"], phase_rows, numeric_columns={1}),
|
||||
"",
|
||||
])
|
||||
|
||||
loads_by_format_rows = []
|
||||
for key, metrics in sort_rows_by_metric(report.section("loads_by_format"), "time_ms", top):
|
||||
loads_by_format_rows.append([
|
||||
key,
|
||||
format_count(metrics.get("count", 0.0)),
|
||||
format_ms(metrics.get("time_ms", 0.0)),
|
||||
format_bytes(metrics.get("bytes", 0.0)),
|
||||
format_count(metrics.get("fail", 0.0)),
|
||||
])
|
||||
if loads_by_format_rows:
|
||||
lines.extend([
|
||||
"Loads By Format",
|
||||
render_table(
|
||||
["format", "count", "time_ms", "bytes", "fail"],
|
||||
loads_by_format_rows,
|
||||
numeric_columns={1, 2, 3, 4},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
mounts_by_format_rows = []
|
||||
for key, metrics in sort_rows_by_metric(report.section("mounts_by_format"), "time_ms", top):
|
||||
mounts_by_format_rows.append([
|
||||
key,
|
||||
format_count(metrics.get("count", 0.0)),
|
||||
format_ms(metrics.get("time_ms", 0.0)),
|
||||
format_count(metrics.get("entries", 0.0)),
|
||||
format_count(metrics.get("fail", 0.0)),
|
||||
])
|
||||
if mounts_by_format_rows:
|
||||
lines.extend([
|
||||
"Mounts By Format",
|
||||
render_table(
|
||||
["format", "count", "time_ms", "entries", "fail"],
|
||||
mounts_by_format_rows,
|
||||
numeric_columns={1, 2, 3, 4},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
phase_load_rows = []
|
||||
for key, metrics in sort_rows_by_metric(aggregate_phase_loads(report), "time_ms", top):
|
||||
phase_load_rows.append([
|
||||
key,
|
||||
format_count(metrics.get("count", 0.0)),
|
||||
format_ms(metrics.get("time_ms", 0.0)),
|
||||
format_bytes(metrics.get("bytes", 0.0)),
|
||||
format_count(metrics.get("fail", 0.0)),
|
||||
])
|
||||
if phase_load_rows:
|
||||
lines.extend([
|
||||
"Load Time By Phase",
|
||||
render_table(
|
||||
["phase", "count", "time_ms", "bytes", "fail"],
|
||||
phase_load_rows,
|
||||
numeric_columns={1, 2, 3, 4},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
stage_rows = []
|
||||
for key, metrics in sort_rows_by_metric(report.section("loader_stages"), "time_ms", top):
|
||||
count = metrics.get("count", 0.0)
|
||||
avg_us = (metrics.get("time_ms", 0.0) * 1000.0 / count) if count else 0.0
|
||||
stage_rows.append([
|
||||
key,
|
||||
format_count(count),
|
||||
format_ms(metrics.get("time_ms", 0.0)),
|
||||
f"{avg_us:.1f}",
|
||||
format_bytes(metrics.get("in", 0.0)),
|
||||
format_bytes(metrics.get("out", 0.0)),
|
||||
])
|
||||
if stage_rows:
|
||||
lines.extend([
|
||||
"Top Loader Stages",
|
||||
render_table(
|
||||
["stage", "count", "time_ms", "avg_us", "in", "out"],
|
||||
stage_rows,
|
||||
numeric_columns={1, 2, 3, 4, 5},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
hot_pack_rows = []
|
||||
for key, metrics in sort_rows_by_metric(report.section("top_phase_pack_loads"), "time_ms", top):
|
||||
hot_pack_rows.append([
|
||||
key,
|
||||
format_count(metrics.get("count", 0.0)),
|
||||
format_ms(metrics.get("time_ms", 0.0)),
|
||||
format_bytes(metrics.get("bytes", 0.0)),
|
||||
])
|
||||
if hot_pack_rows:
|
||||
lines.extend([
|
||||
"Top Phase Pack Loads",
|
||||
render_table(
|
||||
["phase | pack", "count", "time_ms", "bytes"],
|
||||
hot_pack_rows,
|
||||
numeric_columns={1, 2, 3},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
extension_rows = []
|
||||
for key, metrics in sort_rows_by_metric(report.section("top_extension_loads"), "time_ms", top):
|
||||
extension_rows.append([
|
||||
key,
|
||||
format_count(metrics.get("count", 0.0)),
|
||||
format_ms(metrics.get("time_ms", 0.0)),
|
||||
format_bytes(metrics.get("bytes", 0.0)),
|
||||
])
|
||||
if extension_rows:
|
||||
lines.extend([
|
||||
"Top Extensions",
|
||||
render_table(
|
||||
["extension", "count", "time_ms", "bytes"],
|
||||
extension_rows,
|
||||
numeric_columns={1, 2, 3},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def compare_two_reports(left: Report, right: Report, top: int) -> str:
|
||||
lines = [f"== {left.label} vs {right.label} ==", ""]
|
||||
|
||||
overview_rows = []
|
||||
for report in (left, right):
|
||||
load_totals = sum_section_metrics(report.section("loads_by_format"))
|
||||
mount_totals = sum_section_metrics(report.section("mounts_by_format"))
|
||||
overview_rows.append([
|
||||
report.label,
|
||||
format_ms(report.uptime_ms()),
|
||||
format_count(load_totals.get("count", 0.0)),
|
||||
format_ms(load_totals.get("time_ms", 0.0)),
|
||||
format_bytes(load_totals.get("bytes", 0.0)),
|
||||
format_count(mount_totals.get("count", 0.0)),
|
||||
format_ms(mount_totals.get("time_ms", 0.0)),
|
||||
])
|
||||
lines.extend([
|
||||
"Overview",
|
||||
render_table(
|
||||
["report", "uptime_ms", "loads", "load_ms", "load_bytes", "mounts", "mount_ms"],
|
||||
overview_rows,
|
||||
numeric_columns={1, 2, 3, 4, 5, 6},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
left_packed_loads = sum_selected_section_metrics(left.section("loads_by_format"), exclude_keys={"disk"})
|
||||
right_packed_loads = sum_selected_section_metrics(right.section("loads_by_format"), exclude_keys={"disk"})
|
||||
packed_load_rows = [[
|
||||
format_count(left_packed_loads.get("count", 0.0)),
|
||||
format_ms(left_packed_loads.get("time_ms", 0.0)),
|
||||
format_bytes(left_packed_loads.get("bytes", 0.0)),
|
||||
format_count(right_packed_loads.get("count", 0.0)),
|
||||
format_ms(right_packed_loads.get("time_ms", 0.0)),
|
||||
format_bytes(right_packed_loads.get("bytes", 0.0)),
|
||||
format_delta(right_packed_loads.get("time_ms", 0.0) - left_packed_loads.get("time_ms", 0.0)),
|
||||
format_percent(relative_delta_percent(left_packed_loads.get("time_ms", 0.0), right_packed_loads.get("time_ms", 0.0))),
|
||||
]]
|
||||
lines.extend([
|
||||
"Packed Load Totals",
|
||||
render_table(
|
||||
[
|
||||
f"{left.label}_count",
|
||||
f"{left.label}_ms",
|
||||
f"{left.label}_bytes",
|
||||
f"{right.label}_count",
|
||||
f"{right.label}_ms",
|
||||
f"{right.label}_bytes",
|
||||
"delta_ms",
|
||||
"delta_pct",
|
||||
],
|
||||
packed_load_rows,
|
||||
numeric_columns={0, 1, 2, 3, 4, 5, 6, 7},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
left_packed_mounts = sum_selected_section_metrics(left.section("mounts_by_format"))
|
||||
right_packed_mounts = sum_selected_section_metrics(right.section("mounts_by_format"))
|
||||
packed_mount_rows = [[
|
||||
format_count(left_packed_mounts.get("count", 0.0)),
|
||||
format_ms(left_packed_mounts.get("time_ms", 0.0)),
|
||||
format_count(left_packed_mounts.get("entries", 0.0)),
|
||||
format_count(right_packed_mounts.get("count", 0.0)),
|
||||
format_ms(right_packed_mounts.get("time_ms", 0.0)),
|
||||
format_count(right_packed_mounts.get("entries", 0.0)),
|
||||
format_delta(right_packed_mounts.get("time_ms", 0.0) - left_packed_mounts.get("time_ms", 0.0)),
|
||||
format_percent(relative_delta_percent(left_packed_mounts.get("time_ms", 0.0), right_packed_mounts.get("time_ms", 0.0))),
|
||||
]]
|
||||
if packed_mount_rows:
|
||||
lines.extend([
|
||||
"Packed Mount Totals",
|
||||
render_table(
|
||||
[
|
||||
f"{left.label}_count",
|
||||
f"{left.label}_ms",
|
||||
f"{left.label}_entries",
|
||||
f"{right.label}_count",
|
||||
f"{right.label}_ms",
|
||||
f"{right.label}_entries",
|
||||
"delta_ms",
|
||||
"delta_pct",
|
||||
],
|
||||
packed_mount_rows,
|
||||
numeric_columns={0, 1, 2, 3, 4, 5, 6, 7},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
left_phases = dict(left.phase_markers)
|
||||
right_phases = dict(right.phase_markers)
|
||||
ordered_phases = [phase for phase, _ in left.phase_markers]
|
||||
ordered_phases.extend(phase for phase, _ in right.phase_markers if phase not in left_phases)
|
||||
phase_rows = []
|
||||
for phase in ordered_phases:
|
||||
left_value = left_phases.get(phase)
|
||||
right_value = right_phases.get(phase)
|
||||
if left_value is None and right_value is None:
|
||||
continue
|
||||
delta = (right_value or 0.0) - (left_value or 0.0)
|
||||
delta_pct = relative_delta_percent(left_value or 0.0, right_value or 0.0)
|
||||
phase_rows.append([
|
||||
phase,
|
||||
"-" if left_value is None else format_ms(left_value),
|
||||
"-" if right_value is None else format_ms(right_value),
|
||||
format_delta(delta),
|
||||
format_percent(delta_pct),
|
||||
])
|
||||
if phase_rows:
|
||||
lines.extend([
|
||||
"Phase Markers",
|
||||
render_table(
|
||||
["phase", left.label, right.label, "delta_ms", "delta_pct"],
|
||||
phase_rows,
|
||||
numeric_columns={1, 2, 3, 4},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
left_phase_loads = aggregate_phase_loads(left)
|
||||
right_phase_loads = aggregate_phase_loads(right)
|
||||
phase_names = sorted(set(left_phase_loads) | set(right_phase_loads))
|
||||
phase_load_rows = []
|
||||
for phase in phase_names:
|
||||
left_metrics = left_phase_loads.get(phase, {})
|
||||
right_metrics = right_phase_loads.get(phase, {})
|
||||
left_time = left_metrics.get("time_ms", 0.0)
|
||||
right_time = right_metrics.get("time_ms", 0.0)
|
||||
phase_load_rows.append([
|
||||
phase,
|
||||
format_ms(left_time),
|
||||
format_ms(right_time),
|
||||
format_delta(right_time - left_time),
|
||||
format_percent(relative_delta_percent(left_time, right_time)),
|
||||
format_count(left_metrics.get("count", 0.0)),
|
||||
format_count(right_metrics.get("count", 0.0)),
|
||||
])
|
||||
if phase_load_rows:
|
||||
phase_load_rows.sort(key=lambda row: (-max(parse_float(row[1]), parse_float(row[2])), row[0]))
|
||||
lines.extend([
|
||||
"Load Time By Phase",
|
||||
render_table(
|
||||
["phase", f"{left.label}_ms", f"{right.label}_ms", "delta_ms", "delta_pct", f"{left.label}_count", f"{right.label}_count"],
|
||||
phase_load_rows,
|
||||
numeric_columns={1, 2, 3, 4, 5, 6},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
format_names = sorted(set(left.section("loads_by_format")) | set(right.section("loads_by_format")))
|
||||
format_rows = []
|
||||
for format_name in format_names:
|
||||
left_metrics = left.section("loads_by_format").get(format_name, {})
|
||||
right_metrics = right.section("loads_by_format").get(format_name, {})
|
||||
left_time = left_metrics.get("time_ms", 0.0)
|
||||
right_time = right_metrics.get("time_ms", 0.0)
|
||||
format_rows.append([
|
||||
format_name,
|
||||
format_count(left_metrics.get("count", 0.0)),
|
||||
format_ms(left_time),
|
||||
format_bytes(left_metrics.get("bytes", 0.0)),
|
||||
format_count(right_metrics.get("count", 0.0)),
|
||||
format_ms(right_time),
|
||||
format_bytes(right_metrics.get("bytes", 0.0)),
|
||||
format_delta(right_time - left_time),
|
||||
format_percent(relative_delta_percent(left_time, right_time)),
|
||||
])
|
||||
if format_rows:
|
||||
format_rows.sort(key=lambda row: (-max(parse_float(row[2]), parse_float(row[5])), row[0]))
|
||||
lines.extend([
|
||||
"Loads By Format",
|
||||
render_table(
|
||||
[
|
||||
"format",
|
||||
f"{left.label}_count",
|
||||
f"{left.label}_ms",
|
||||
f"{left.label}_bytes",
|
||||
f"{right.label}_count",
|
||||
f"{right.label}_ms",
|
||||
f"{right.label}_bytes",
|
||||
"delta_ms",
|
||||
"delta_pct",
|
||||
],
|
||||
format_rows,
|
||||
numeric_columns={1, 2, 3, 4, 5, 6, 7, 8},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
mount_names = sorted(set(left.section("mounts_by_format")) | set(right.section("mounts_by_format")))
|
||||
mount_rows = []
|
||||
for format_name in mount_names:
|
||||
left_metrics = left.section("mounts_by_format").get(format_name, {})
|
||||
right_metrics = right.section("mounts_by_format").get(format_name, {})
|
||||
left_time = left_metrics.get("time_ms", 0.0)
|
||||
right_time = right_metrics.get("time_ms", 0.0)
|
||||
mount_rows.append([
|
||||
format_name,
|
||||
format_count(left_metrics.get("count", 0.0)),
|
||||
format_ms(left_time),
|
||||
format_count(left_metrics.get("entries", 0.0)),
|
||||
format_count(right_metrics.get("count", 0.0)),
|
||||
format_ms(right_time),
|
||||
format_count(right_metrics.get("entries", 0.0)),
|
||||
format_delta(right_time - left_time),
|
||||
format_percent(relative_delta_percent(left_time, right_time)),
|
||||
])
|
||||
if mount_rows:
|
||||
mount_rows.sort(key=lambda row: (-max(parse_float(row[2]), parse_float(row[5])), row[0]))
|
||||
lines.extend([
|
||||
"Mounts By Format",
|
||||
render_table(
|
||||
[
|
||||
"format",
|
||||
f"{left.label}_count",
|
||||
f"{left.label}_ms",
|
||||
f"{left.label}_entries",
|
||||
f"{right.label}_count",
|
||||
f"{right.label}_ms",
|
||||
f"{right.label}_entries",
|
||||
"delta_ms",
|
||||
"delta_pct",
|
||||
],
|
||||
mount_rows,
|
||||
numeric_columns={1, 2, 3, 4, 5, 6, 7, 8},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
stage_names = sorted(set(left.section("loader_stages")) | set(right.section("loader_stages")))
|
||||
stage_rows = []
|
||||
for stage_name in stage_names:
|
||||
left_metrics = left.section("loader_stages").get(stage_name, {})
|
||||
right_metrics = right.section("loader_stages").get(stage_name, {})
|
||||
left_time = left_metrics.get("time_ms", 0.0)
|
||||
right_time = right_metrics.get("time_ms", 0.0)
|
||||
left_count = left_metrics.get("count", 0.0)
|
||||
right_count = right_metrics.get("count", 0.0)
|
||||
left_avg_us = (left_time * 1000.0 / left_count) if left_count else 0.0
|
||||
right_avg_us = (right_time * 1000.0 / right_count) if right_count else 0.0
|
||||
stage_rows.append([
|
||||
stage_name,
|
||||
format_ms(left_time),
|
||||
f"{left_avg_us:.1f}",
|
||||
format_ms(right_time),
|
||||
f"{right_avg_us:.1f}",
|
||||
format_delta(right_time - left_time),
|
||||
format_percent(relative_delta_percent(left_time, right_time)),
|
||||
])
|
||||
if stage_rows:
|
||||
stage_rows.sort(key=lambda row: (-max(parse_float(row[1]), parse_float(row[3])), row[0]))
|
||||
lines.extend([
|
||||
"Loader Stages",
|
||||
render_table(
|
||||
[
|
||||
"stage",
|
||||
f"{left.label}_ms",
|
||||
f"{left.label}_avg_us",
|
||||
f"{right.label}_ms",
|
||||
f"{right.label}_avg_us",
|
||||
"delta_ms",
|
||||
"delta_pct",
|
||||
],
|
||||
stage_rows[:top],
|
||||
numeric_columns={1, 2, 3, 4, 5, 6},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
for report in (left, right):
|
||||
hot_rows = []
|
||||
for key, metrics in sort_rows_by_metric(report.section("top_phase_pack_loads"), "time_ms", top):
|
||||
hot_rows.append([
|
||||
key,
|
||||
format_count(metrics.get("count", 0.0)),
|
||||
format_ms(metrics.get("time_ms", 0.0)),
|
||||
format_bytes(metrics.get("bytes", 0.0)),
|
||||
])
|
||||
if hot_rows:
|
||||
lines.extend([
|
||||
f"Top Phase Pack Loads: {report.label}",
|
||||
render_table(
|
||||
["phase | pack", "count", "time_ms", "bytes"],
|
||||
hot_rows,
|
||||
numeric_columns={1, 2, 3},
|
||||
),
|
||||
"",
|
||||
])
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def summarize_many_reports(reports: list[Report], top: int) -> str:
|
||||
if len(reports) == 2:
|
||||
return compare_two_reports(reports[0], reports[1], top)
|
||||
|
||||
parts = []
|
||||
for report in reports:
|
||||
parts.append(summarize_report(report, top))
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
try:
|
||||
reports = [load_report(value) for value in args.reports]
|
||||
except FileNotFoundError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(summarize_many_reports(reports, max(args.top, 1)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -65,6 +65,11 @@ VOID ELTimer_SetFrameMSec()
|
||||
gs_dwFrameTime = ELTimer_GetMSec();
|
||||
}
|
||||
|
||||
VOID ELTimer_SetFrameMSecValue(DWORD dwFrameTime)
|
||||
{
|
||||
gs_dwFrameTime = dwFrameTime;
|
||||
}
|
||||
|
||||
CTimer::CTimer()
|
||||
{
|
||||
ELTimer_Init();
|
||||
|
||||
@@ -38,4 +38,5 @@ VOID ELTimer_SetServerMSec(DWORD dwServerTime);
|
||||
DWORD ELTimer_GetServerMSec();
|
||||
|
||||
VOID ELTimer_SetFrameMSec();
|
||||
DWORD ELTimer_GetFrameMSec();
|
||||
VOID ELTimer_SetFrameMSecValue(DWORD dwFrameTime);
|
||||
DWORD ELTimer_GetFrameMSec();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
file(GLOB_RECURSE FILE_SOURCES "*.h" "*.c" "*.cpp")
|
||||
file(GLOB_RECURSE FILE_SOURCES CONFIGURE_DEPENDS "*.h" "*.c" "*.cpp")
|
||||
|
||||
add_library(PackLib STATIC ${FILE_SOURCES})
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "M2Pack.h"
|
||||
#include "PackProfile.h"
|
||||
#include "M2PackRuntimeKeyProvider.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
#include <zstd.h>
|
||||
@@ -40,6 +42,7 @@ bool ReadPod(const uint8_t* bytes, std::size_t size, std::size_t& offset, T& out
|
||||
|
||||
bool CM2Pack::Load(const std::string& path)
|
||||
{
|
||||
m_source_path = path;
|
||||
std::error_code ec;
|
||||
m_file.map(path, ec);
|
||||
|
||||
@@ -100,6 +103,7 @@ bool CM2Pack::Load(const std::string& path)
|
||||
bool CM2Pack::ValidateManifest()
|
||||
{
|
||||
std::array<uint8_t, M2PACK_HASH_SIZE> manifest_hash {};
|
||||
const auto hashStart = std::chrono::steady_clock::now();
|
||||
crypto_generichash(
|
||||
manifest_hash.data(),
|
||||
manifest_hash.size(),
|
||||
@@ -107,6 +111,13 @@ bool CM2Pack::ValidateManifest()
|
||||
m_manifest_bytes.size(),
|
||||
nullptr,
|
||||
0);
|
||||
RecordPackProfileStage(
|
||||
"m2p",
|
||||
"manifest_hash",
|
||||
m_manifest_bytes.size(),
|
||||
manifest_hash.size(),
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - hashStart).count()));
|
||||
|
||||
if (memcmp(manifest_hash.data(), m_header.manifest_hash, manifest_hash.size()) != 0)
|
||||
{
|
||||
@@ -121,17 +132,33 @@ bool CM2Pack::ValidateManifest()
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto verifyStart = std::chrono::steady_clock::now();
|
||||
if (crypto_sign_verify_detached(
|
||||
m_header.manifest_signature,
|
||||
m_manifest_bytes.data(),
|
||||
m_manifest_bytes.size(),
|
||||
publicKey->data()) != 0)
|
||||
{
|
||||
RecordPackProfileStage(
|
||||
"m2p",
|
||||
"manifest_signature",
|
||||
m_manifest_bytes.size(),
|
||||
M2PACK_SIGNATURE_SIZE,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - verifyStart).count()));
|
||||
TraceError("CM2Pack::ValidateManifest: manifest signature mismatch");
|
||||
return false;
|
||||
}
|
||||
RecordPackProfileStage(
|
||||
"m2p",
|
||||
"manifest_signature",
|
||||
m_manifest_bytes.size(),
|
||||
M2PACK_SIGNATURE_SIZE,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - verifyStart).count()));
|
||||
|
||||
std::size_t offset = 0;
|
||||
const auto parseStart = std::chrono::steady_clock::now();
|
||||
TM2PackManifestHeader manifest_header {};
|
||||
if (!ReadPod(m_manifest_bytes.data(), m_manifest_bytes.size(), offset, manifest_header))
|
||||
{
|
||||
@@ -184,30 +211,33 @@ bool CM2Pack::ValidateManifest()
|
||||
m_index.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
RecordPackProfileStage(
|
||||
"m2p",
|
||||
"manifest_parse",
|
||||
m_manifest_bytes.size(),
|
||||
m_index.size(),
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - parseStart).count()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CM2Pack::DecryptEntryPayload(const TM2PackEntry& entry, std::vector<uint8_t>& decrypted, CBufferPool* pPool)
|
||||
{
|
||||
const uint64_t begin = sizeof(TM2PackHeader) + entry.data_offset;
|
||||
const auto* ciphertext = reinterpret_cast<const uint8_t*>(m_file.data() + begin);
|
||||
|
||||
std::vector<uint8_t> ciphertext_copy;
|
||||
const auto* ciphertext = reinterpret_cast<const unsigned char*>(m_file.data() + begin);
|
||||
if (pPool)
|
||||
{
|
||||
ciphertext_copy = pPool->Acquire(entry.stored_size);
|
||||
decrypted = pPool->Acquire(entry.stored_size);
|
||||
}
|
||||
ciphertext_copy.resize(entry.stored_size);
|
||||
memcpy(ciphertext_copy.data(), ciphertext, entry.stored_size);
|
||||
|
||||
decrypted.resize(entry.stored_size);
|
||||
const auto decryptStart = std::chrono::steady_clock::now();
|
||||
unsigned long long written = 0;
|
||||
if (crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
decrypted.data(),
|
||||
&written,
|
||||
nullptr,
|
||||
ciphertext_copy.data(),
|
||||
ciphertext_copy.size(),
|
||||
ciphertext,
|
||||
entry.stored_size,
|
||||
reinterpret_cast<const unsigned char*>(entry.path.data()),
|
||||
entry.path.size(),
|
||||
entry.nonce.data(),
|
||||
@@ -215,16 +245,19 @@ bool CM2Pack::DecryptEntryPayload(const TM2PackEntry& entry, std::vector<uint8_t
|
||||
{
|
||||
if (pPool)
|
||||
{
|
||||
pPool->Release(std::move(ciphertext_copy));
|
||||
pPool->Release(std::move(decrypted));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
RecordPackProfileStage(
|
||||
"m2p",
|
||||
"aead_decrypt",
|
||||
entry.stored_size,
|
||||
written,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - decryptStart).count()));
|
||||
|
||||
decrypted.resize(static_cast<std::size_t>(written));
|
||||
if (pPool)
|
||||
{
|
||||
pPool->Release(std::move(ciphertext_copy));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -252,7 +285,19 @@ bool CM2Pack::GetFileWithPool(const TM2PackEntry& entry, std::vector<uint8_t>& r
|
||||
{
|
||||
result.resize(entry.original_size);
|
||||
ZSTD_DCtx* dctx = GetThreadLocalZstdContext();
|
||||
const auto decompressStart = std::chrono::steady_clock::now();
|
||||
size_t written = ZSTD_decompressDCtx(dctx, result.data(), result.size(), compressed.data(), compressed.size());
|
||||
RecordPackProfileStage(
|
||||
"m2p",
|
||||
"zstd_decompress",
|
||||
compressed.size(),
|
||||
ZSTD_isError(written) ? 0 : written,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - decompressStart).count()));
|
||||
if (pPool)
|
||||
{
|
||||
pPool->Release(std::move(compressed));
|
||||
}
|
||||
if (ZSTD_isError(written) || written != entry.original_size)
|
||||
{
|
||||
TraceError("CM2Pack::GetFileWithPool: zstd failed for '%s'", entry.path.c_str());
|
||||
@@ -262,11 +307,21 @@ bool CM2Pack::GetFileWithPool(const TM2PackEntry& entry, std::vector<uint8_t>& r
|
||||
}
|
||||
|
||||
default:
|
||||
if (pPool)
|
||||
{
|
||||
pPool->Release(std::move(compressed));
|
||||
}
|
||||
TraceError("CM2Pack::GetFileWithPool: unsupported compression %u for '%s'", entry.compression, entry.path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ShouldVerifyM2PackPlaintextHash())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<uint8_t, M2PACK_HASH_SIZE> plain_hash {};
|
||||
const auto hashStart = std::chrono::steady_clock::now();
|
||||
crypto_generichash(
|
||||
plain_hash.data(),
|
||||
plain_hash.size(),
|
||||
@@ -274,6 +329,13 @@ bool CM2Pack::GetFileWithPool(const TM2PackEntry& entry, std::vector<uint8_t>& r
|
||||
result.size(),
|
||||
nullptr,
|
||||
0);
|
||||
RecordPackProfileStage(
|
||||
"m2p",
|
||||
"plaintext_hash",
|
||||
result.size(),
|
||||
plain_hash.size(),
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - hashStart).count()));
|
||||
|
||||
if (memcmp(plain_hash.data(), entry.plaintext_hash.data(), plain_hash.size()) != 0)
|
||||
{
|
||||
|
||||
@@ -72,6 +72,7 @@ public:
|
||||
|
||||
bool Load(const std::string& path);
|
||||
const std::vector<TM2PackEntry>& GetIndex() const { return m_index; }
|
||||
const std::string& GetSourcePath() const { return m_source_path; }
|
||||
|
||||
bool GetFile(const TM2PackEntry& entry, std::vector<uint8_t>& result);
|
||||
bool GetFileWithPool(const TM2PackEntry& entry, std::vector<uint8_t>& result, CBufferPool* pPool);
|
||||
@@ -82,6 +83,7 @@ private:
|
||||
|
||||
private:
|
||||
TM2PackHeader m_header {};
|
||||
std::string m_source_path;
|
||||
std::vector<uint8_t> m_manifest_bytes;
|
||||
std::vector<TM2PackEntry> m_index;
|
||||
mio::mmap_source m_file;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "M2PackRuntimeKeyProvider.h"
|
||||
#include "PackProfile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
@@ -18,6 +20,7 @@ constexpr const char* M2PACK_ENV_MASTER_KEY = "M2PACK_MASTER_KEY_HEX";
|
||||
constexpr const char* M2PACK_ENV_PUBLIC_KEY = "M2PACK_SIGN_PUBKEY_HEX";
|
||||
constexpr const char* M2PACK_ENV_MAP_NAME = "M2PACK_KEY_MAP";
|
||||
constexpr const char* M2PACK_ENV_KEY_ID = "M2PACK_KEY_ID";
|
||||
constexpr const char* M2PACK_ENV_STRICT_HASH = "M2PACK_STRICT_HASH";
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct M2PackSharedKeys
|
||||
@@ -38,6 +41,7 @@ struct RuntimeKeyState
|
||||
std::array<uint8_t, M2PACK_PUBLIC_KEY_SIZE> public_key {};
|
||||
bool runtime_master_key = false;
|
||||
bool runtime_public_key = false;
|
||||
bool verify_plaintext_hash = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
@@ -100,6 +104,31 @@ uint32_t ParseUInt32(const std::string& value)
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseBoolFlag(std::string value, bool& out)
|
||||
{
|
||||
value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
}), value.end());
|
||||
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
|
||||
if (value == "1" || value == "true" || value == "yes" || value == "on")
|
||||
{
|
||||
out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value == "0" || value == "false" || value == "no" || value == "off")
|
||||
{
|
||||
out = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::array<uint8_t, M2PACK_PUBLIC_KEY_SIZE>* FindCompiledPublicKey(uint32_t keyId)
|
||||
{
|
||||
for (std::size_t i = 0; i < M2PACK_SIGN_KEY_IDS.size(); ++i)
|
||||
@@ -191,16 +220,34 @@ void ApplyCommandLineOption(const std::string& key, const std::string& value)
|
||||
LoadFromSharedMapping(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key == "--m2pack-strict-hash")
|
||||
{
|
||||
bool enabled = false;
|
||||
if (ParseBoolFlag(value, enabled))
|
||||
{
|
||||
g_state.verify_plaintext_hash = enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
TraceError("Invalid value for --m2pack-strict-hash");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool InitializeM2PackRuntimeKeyProvider(const char* commandLine)
|
||||
{
|
||||
InitializePackProfile(commandLine);
|
||||
|
||||
if (g_state.initialized)
|
||||
return true;
|
||||
|
||||
g_state = RuntimeKeyState {};
|
||||
#ifdef _DEBUG
|
||||
g_state.verify_plaintext_hash = true;
|
||||
#endif
|
||||
|
||||
const std::string mapName = GetEnvString(M2PACK_ENV_MAP_NAME);
|
||||
if (!mapName.empty())
|
||||
@@ -240,6 +287,16 @@ bool InitializeM2PackRuntimeKeyProvider(const char* commandLine)
|
||||
TraceError("Invalid M2PACK_SIGN_PUBKEY_HEX value");
|
||||
}
|
||||
|
||||
const std::string envStrictHash = GetEnvString(M2PACK_ENV_STRICT_HASH);
|
||||
if (!envStrictHash.empty())
|
||||
{
|
||||
bool enabled = false;
|
||||
if (ParseBoolFlag(envStrictHash, enabled))
|
||||
g_state.verify_plaintext_hash = enabled;
|
||||
else
|
||||
TraceError("Invalid M2PACK_STRICT_HASH value");
|
||||
}
|
||||
|
||||
int argc = 0;
|
||||
PCHAR* argv = CommandLineToArgv(const_cast<PCHAR>(commandLine ? commandLine : ""), &argc);
|
||||
if (argv)
|
||||
@@ -247,6 +304,31 @@ bool InitializeM2PackRuntimeKeyProvider(const char* commandLine)
|
||||
for (int i = 0; i < argc; ++i)
|
||||
{
|
||||
const std::string key = argv[i];
|
||||
if (key == "--m2pack-strict-hash")
|
||||
{
|
||||
bool enabled = true;
|
||||
if (i + 1 < argc)
|
||||
{
|
||||
const std::string next = argv[i + 1];
|
||||
if (!next.starts_with("--"))
|
||||
{
|
||||
if (!ParseBoolFlag(next, enabled))
|
||||
{
|
||||
TraceError("Invalid value for --m2pack-strict-hash");
|
||||
}
|
||||
else
|
||||
{
|
||||
g_state.verify_plaintext_hash = enabled;
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
g_state.verify_plaintext_hash = enabled;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((key == "--m2pack-key-hex" || key == "--m2pack-pubkey-hex" || key == "--m2pack-key-map" || key == "--m2pack-key-id") && i + 1 < argc)
|
||||
{
|
||||
ApplyCommandLineOption(key, argv[i + 1]);
|
||||
@@ -268,6 +350,11 @@ bool InitializeM2PackRuntimeKeyProvider(const char* commandLine)
|
||||
Tracef("M2Pack runtime key provider: no runtime master key available; .m2p loading will be denied\n");
|
||||
}
|
||||
|
||||
if (g_state.verify_plaintext_hash)
|
||||
{
|
||||
Tracef("M2Pack runtime key provider: plaintext hash verification enabled\n");
|
||||
}
|
||||
|
||||
g_state.initialized = true;
|
||||
return true;
|
||||
}
|
||||
@@ -311,3 +398,8 @@ bool IsM2PackUsingRuntimePublicKey()
|
||||
{
|
||||
return g_state.runtime_public_key;
|
||||
}
|
||||
|
||||
bool ShouldVerifyM2PackPlaintextHash()
|
||||
{
|
||||
return g_state.verify_plaintext_hash;
|
||||
}
|
||||
|
||||
@@ -12,3 +12,4 @@ bool HasM2PackRuntimeMasterKey();
|
||||
bool HasM2PackRuntimeKeysForArchiveLoad(uint32_t keyId);
|
||||
bool IsM2PackUsingRuntimeMasterKey();
|
||||
bool IsM2PackUsingRuntimePublicKey();
|
||||
bool ShouldVerifyM2PackPlaintextHash();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "Pack.h"
|
||||
#include "PackProfile.h"
|
||||
#include "EterLib/BufferPool.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <zstd.h>
|
||||
|
||||
static thread_local ZSTD_DCtx* g_zstdDCtx = nullptr;
|
||||
@@ -20,6 +23,7 @@ void CPack::DecryptData(uint8_t* data, size_t len, const uint8_t* nonce)
|
||||
|
||||
bool CPack::Load(const std::string& path)
|
||||
{
|
||||
m_source_path = path;
|
||||
std::error_code ec;
|
||||
m_file.map(path, ec);
|
||||
|
||||
@@ -68,7 +72,15 @@ bool CPack::GetFileWithPool(const TPackFileEntry& entry, TPackFile& result, CBuf
|
||||
switch (entry.encryption)
|
||||
{
|
||||
case 0: {
|
||||
const auto decompressStart = std::chrono::steady_clock::now();
|
||||
size_t decompressed_size = ZSTD_decompressDCtx(dctx, result.data(), result.size(), m_file.data() + offset, entry.compressed_size);
|
||||
RecordPackProfileStage(
|
||||
"pck",
|
||||
"zstd_decompress",
|
||||
entry.compressed_size,
|
||||
ZSTD_isError(decompressed_size) ? 0 : decompressed_size,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - decompressStart).count()));
|
||||
if (decompressed_size != entry.file_size) {
|
||||
return false;
|
||||
}
|
||||
@@ -83,9 +95,25 @@ bool CPack::GetFileWithPool(const TPackFileEntry& entry, TPackFile& result, CBuf
|
||||
|
||||
memcpy(compressed_data.data(), m_file.data() + offset, entry.compressed_size);
|
||||
|
||||
const auto decryptStart = std::chrono::steady_clock::now();
|
||||
DecryptData(compressed_data.data(), entry.compressed_size, entry.nonce);
|
||||
RecordPackProfileStage(
|
||||
"pck",
|
||||
"xchacha20_decrypt",
|
||||
entry.compressed_size,
|
||||
entry.compressed_size,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - decryptStart).count()));
|
||||
|
||||
const auto decompressStart = std::chrono::steady_clock::now();
|
||||
size_t decompressed_size = ZSTD_decompressDCtx(dctx, result.data(), result.size(), compressed_data.data(), compressed_data.size());
|
||||
RecordPackProfileStage(
|
||||
"pck",
|
||||
"zstd_decompress",
|
||||
compressed_data.size(),
|
||||
ZSTD_isError(decompressed_size) ? 0 : decompressed_size,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - decompressStart).count()));
|
||||
|
||||
if (pPool) {
|
||||
pPool->Release(std::move(compressed_data));
|
||||
|
||||
@@ -14,6 +14,7 @@ public:
|
||||
|
||||
bool Load(const std::string& path);
|
||||
const std::vector<TPackFileEntry>& GetIndex() const { return m_index; }
|
||||
const std::string& GetSourcePath() const { return m_source_path; }
|
||||
|
||||
bool GetFile(const TPackFileEntry& entry, TPackFile& result);
|
||||
bool GetFileWithPool(const TPackFileEntry& entry, TPackFile& result, CBufferPool* pPool);
|
||||
@@ -22,6 +23,7 @@ private:
|
||||
void DecryptData(uint8_t* data, size_t len, const uint8_t* nonce);
|
||||
|
||||
TPackFileHeader m_header;
|
||||
std::string m_source_path;
|
||||
std::vector<TPackFileEntry> m_index;
|
||||
mio::mmap_source m_file;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "PackManager.h"
|
||||
#include "PackProfile.h"
|
||||
#include "EterLib/BufferPool.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
@@ -27,8 +30,16 @@ bool CPackManager::AddPack(const std::string& path)
|
||||
if (packPath.extension() == ".m2p")
|
||||
{
|
||||
std::shared_ptr<CM2Pack> pack = std::make_shared<CM2Pack>();
|
||||
const auto loadStart = std::chrono::steady_clock::now();
|
||||
if (!pack->Load(path))
|
||||
{
|
||||
RecordPackProfileMount(
|
||||
"m2p",
|
||||
path,
|
||||
false,
|
||||
0,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -38,14 +49,29 @@ bool CPackManager::AddPack(const std::string& path)
|
||||
{
|
||||
m_m2_entries[entry.path] = std::make_pair(pack, entry);
|
||||
}
|
||||
RecordPackProfileMount(
|
||||
"m2p",
|
||||
path,
|
||||
true,
|
||||
index.size(),
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<CPack> pack = std::make_shared<CPack>();
|
||||
const auto loadStart = std::chrono::steady_clock::now();
|
||||
|
||||
if (!pack->Load(path))
|
||||
{
|
||||
RecordPackProfileMount(
|
||||
"pck",
|
||||
path,
|
||||
false,
|
||||
0,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,6 +81,13 @@ bool CPackManager::AddPack(const std::string& path)
|
||||
{
|
||||
m_entries[entry.file_name] = std::make_pair(pack, entry);
|
||||
}
|
||||
RecordPackProfileMount(
|
||||
"pck",
|
||||
path,
|
||||
true,
|
||||
index.size(),
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -73,16 +106,37 @@ bool CPackManager::GetFileWithPool(std::string_view path, TPackFile& result, CBu
|
||||
if (m_load_from_pack) {
|
||||
auto m2it = m_m2_entries.find(buf);
|
||||
if (m2it != m_m2_entries.end()) {
|
||||
return m2it->second.first->GetFileWithPool(m2it->second.second, result, pPool);
|
||||
const auto loadStart = std::chrono::steady_clock::now();
|
||||
const bool ok = m2it->second.first->GetFileWithPool(m2it->second.second, result, pPool);
|
||||
RecordPackProfileLoad(
|
||||
"m2p",
|
||||
m2it->second.first->GetSourcePath(),
|
||||
buf,
|
||||
ok,
|
||||
ok ? result.size() : 0,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
return ok;
|
||||
}
|
||||
|
||||
auto it = m_entries.find(buf);
|
||||
if (it != m_entries.end()) {
|
||||
return it->second.first->GetFileWithPool(it->second.second, result, pPool);
|
||||
const auto loadStart = std::chrono::steady_clock::now();
|
||||
const bool ok = it->second.first->GetFileWithPool(it->second.second, result, pPool);
|
||||
RecordPackProfileLoad(
|
||||
"pck",
|
||||
it->second.first->GetSourcePath(),
|
||||
buf,
|
||||
ok,
|
||||
ok ? result.size() : 0,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to disk (for files not in packs, like bgm folder)
|
||||
const auto loadStart = std::chrono::steady_clock::now();
|
||||
std::ifstream ifs(buf, std::ios::binary);
|
||||
if (ifs.is_open()) {
|
||||
ifs.seekg(0, std::ios::end);
|
||||
@@ -97,10 +151,27 @@ bool CPackManager::GetFileWithPool(std::string_view path, TPackFile& result, CBu
|
||||
}
|
||||
|
||||
if (ifs.read((char*)result.data(), size)) {
|
||||
RecordPackProfileLoad(
|
||||
"disk",
|
||||
"<disk>",
|
||||
buf,
|
||||
true,
|
||||
result.size(),
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
RecordPackProfileLoad(
|
||||
"disk",
|
||||
"<disk>",
|
||||
buf,
|
||||
false,
|
||||
0,
|
||||
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - loadStart).count()));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
554
src/PackLib/PackProfile.cpp
Normal file
554
src/PackLib/PackProfile.cpp
Normal file
@@ -0,0 +1,554 @@
|
||||
#include "PackProfile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "EterBase/Utils.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
constexpr const char* kPackProfileEnv = "M2PACK_PROFILE";
|
||||
constexpr const char* kPackProfileOutput = "log/pack_profile.txt";
|
||||
constexpr std::size_t kTopListLimit = 12;
|
||||
constexpr auto kSnapshotFlushInterval = std::chrono::seconds(1);
|
||||
|
||||
struct LoadStats
|
||||
{
|
||||
std::uint64_t count = 0;
|
||||
std::uint64_t failures = 0;
|
||||
std::uint64_t output_bytes = 0;
|
||||
std::uint64_t elapsed_us = 0;
|
||||
};
|
||||
|
||||
struct MountStats
|
||||
{
|
||||
std::uint64_t count = 0;
|
||||
std::uint64_t failures = 0;
|
||||
std::uint64_t entry_count = 0;
|
||||
std::uint64_t elapsed_us = 0;
|
||||
};
|
||||
|
||||
struct StageStats
|
||||
{
|
||||
std::uint64_t count = 0;
|
||||
std::uint64_t input_bytes = 0;
|
||||
std::uint64_t output_bytes = 0;
|
||||
std::uint64_t elapsed_us = 0;
|
||||
};
|
||||
|
||||
struct PhaseMarker
|
||||
{
|
||||
std::string phase;
|
||||
std::uint64_t elapsed_us = 0;
|
||||
};
|
||||
|
||||
struct PackProfileState
|
||||
{
|
||||
bool initialized = false;
|
||||
bool enabled = false;
|
||||
std::string current_phase = "startup";
|
||||
Clock::time_point start_time {};
|
||||
std::vector<PhaseMarker> markers;
|
||||
std::map<std::string, LoadStats> load_by_format;
|
||||
std::map<std::string, LoadStats> load_by_phase_format;
|
||||
std::map<std::string, LoadStats> load_by_phase_pack;
|
||||
std::map<std::string, LoadStats> load_by_extension;
|
||||
std::map<std::string, MountStats> mount_by_format;
|
||||
std::map<std::string, MountStats> mount_by_pack;
|
||||
std::map<std::string, StageStats> stage_by_key;
|
||||
bool snapshot_dirty = false;
|
||||
bool stop_snapshot_thread = false;
|
||||
std::condition_variable snapshot_cv;
|
||||
std::thread snapshot_thread;
|
||||
std::mutex mutex;
|
||||
|
||||
~PackProfileState()
|
||||
{
|
||||
StopSnapshotThread();
|
||||
if (enabled)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
WriteReportLocked("shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
void StartSnapshotThread()
|
||||
{
|
||||
snapshot_thread = std::thread([this]() {
|
||||
SnapshotLoop();
|
||||
});
|
||||
}
|
||||
|
||||
void StopSnapshotThread()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
stop_snapshot_thread = true;
|
||||
}
|
||||
snapshot_cv.notify_all();
|
||||
if (snapshot_thread.joinable())
|
||||
{
|
||||
snapshot_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void SnapshotLoop()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
for (;;)
|
||||
{
|
||||
const bool shouldStop = snapshot_cv.wait_for(
|
||||
lock,
|
||||
kSnapshotFlushInterval,
|
||||
[this]() { return stop_snapshot_thread; });
|
||||
if (shouldStop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled || !snapshot_dirty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
WriteReportLocked("periodic");
|
||||
snapshot_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteReportLocked(std::string_view reason) const
|
||||
{
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories("log", ec);
|
||||
|
||||
std::ofstream output(kPackProfileOutput, std::ios::binary | std::ios::trunc);
|
||||
if (!output)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
output << "PACK PROFILE REPORT\n";
|
||||
output << "reason=" << reason << "\n";
|
||||
output << "current_phase=" << current_phase << "\n";
|
||||
|
||||
const auto uptime_us = start_time == Clock::time_point {}
|
||||
? 0
|
||||
: static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
Clock::now() - start_time).count());
|
||||
output << "uptime_ms=" << FormatMs(uptime_us) << "\n\n";
|
||||
|
||||
output << "[phase_markers]\n";
|
||||
for (const auto& marker : markers)
|
||||
{
|
||||
output << marker.phase << "\t" << FormatMs(marker.elapsed_us) << " ms\n";
|
||||
}
|
||||
output << "\n";
|
||||
|
||||
WriteMountSection(output, "[mounts_by_format]", mount_by_format);
|
||||
WriteMountSection(output, "[mounts_by_pack]", mount_by_pack, kTopListLimit);
|
||||
WriteLoadSection(output, "[loads_by_format]", load_by_format);
|
||||
WriteLoadSection(output, "[loads_by_phase_format]", load_by_phase_format);
|
||||
WriteLoadSection(output, "[top_phase_pack_loads]", load_by_phase_pack, kTopListLimit);
|
||||
WriteLoadSection(output, "[top_extension_loads]", load_by_extension, kTopListLimit);
|
||||
WriteStageSection(output, "[loader_stages]", stage_by_key);
|
||||
}
|
||||
|
||||
static std::string FormatMs(std::uint64_t elapsed_us)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out.setf(std::ios::fixed, std::ios::floatfield);
|
||||
out.precision(3);
|
||||
out << (static_cast<double>(elapsed_us) / 1000.0);
|
||||
return out.str();
|
||||
}
|
||||
|
||||
static std::string ToLowerAscii(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string ParsePackLabel(std::string_view pack_path)
|
||||
{
|
||||
const std::filesystem::path path { std::string(pack_path) };
|
||||
const auto filename = path.filename().string();
|
||||
if (!filename.empty())
|
||||
{
|
||||
return ToLowerAscii(filename);
|
||||
}
|
||||
|
||||
return ToLowerAscii(std::string(pack_path));
|
||||
}
|
||||
|
||||
static std::string ParseExtension(std::string_view request_path)
|
||||
{
|
||||
const std::filesystem::path path { std::string(request_path) };
|
||||
auto extension = path.extension().string();
|
||||
if (extension.empty())
|
||||
{
|
||||
return "<none>";
|
||||
}
|
||||
|
||||
return ToLowerAscii(std::move(extension));
|
||||
}
|
||||
|
||||
static std::string MakePhaseKey(std::string_view phase, std::string_view suffix)
|
||||
{
|
||||
std::string key;
|
||||
key.reserve(phase.size() + suffix.size() + 3);
|
||||
key.append(phase);
|
||||
key.append(" | ");
|
||||
key.append(suffix);
|
||||
return key;
|
||||
}
|
||||
|
||||
template <typename MapType>
|
||||
static std::vector<std::pair<std::string, typename MapType::mapped_type>> SortByTimeDesc(const MapType& values)
|
||||
{
|
||||
using StatsType = typename MapType::mapped_type;
|
||||
std::vector<std::pair<std::string, StatsType>> sorted;
|
||||
sorted.reserve(values.size());
|
||||
for (const auto& [key, stats] : values)
|
||||
{
|
||||
sorted.emplace_back(key, stats);
|
||||
}
|
||||
std::sort(sorted.begin(), sorted.end(), [](const auto& left, const auto& right) {
|
||||
if (left.second.elapsed_us != right.second.elapsed_us)
|
||||
{
|
||||
return left.second.elapsed_us > right.second.elapsed_us;
|
||||
}
|
||||
|
||||
return left.first < right.first;
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
static void WriteLoadSection(std::ofstream& output, std::string_view header, const std::map<std::string, LoadStats>& values, std::size_t limit = 0)
|
||||
{
|
||||
output << header << "\n";
|
||||
const auto sorted = SortByTimeDesc(values);
|
||||
const std::size_t count = limit == 0 ? sorted.size() : std::min(limit, sorted.size());
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
const auto& [key, stats] = sorted[i];
|
||||
output
|
||||
<< key
|
||||
<< "\tcount=" << stats.count
|
||||
<< "\tfail=" << stats.failures
|
||||
<< "\tbytes=" << stats.output_bytes
|
||||
<< "\ttime_ms=" << FormatMs(stats.elapsed_us)
|
||||
<< "\n";
|
||||
}
|
||||
output << "\n";
|
||||
}
|
||||
|
||||
static void WriteMountSection(std::ofstream& output, std::string_view header, const std::map<std::string, MountStats>& values, std::size_t limit = 0)
|
||||
{
|
||||
output << header << "\n";
|
||||
std::vector<std::pair<std::string, MountStats>> sorted(values.begin(), values.end());
|
||||
std::sort(sorted.begin(), sorted.end(), [](const auto& left, const auto& right) {
|
||||
if (left.second.elapsed_us != right.second.elapsed_us)
|
||||
{
|
||||
return left.second.elapsed_us > right.second.elapsed_us;
|
||||
}
|
||||
|
||||
return left.first < right.first;
|
||||
});
|
||||
|
||||
const std::size_t count = limit == 0 ? sorted.size() : std::min(limit, sorted.size());
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
const auto& [key, stats] = sorted[i];
|
||||
output
|
||||
<< key
|
||||
<< "\tcount=" << stats.count
|
||||
<< "\tfail=" << stats.failures
|
||||
<< "\tentries=" << stats.entry_count
|
||||
<< "\ttime_ms=" << FormatMs(stats.elapsed_us)
|
||||
<< "\n";
|
||||
}
|
||||
output << "\n";
|
||||
}
|
||||
|
||||
static void WriteStageSection(std::ofstream& output, std::string_view header, const std::map<std::string, StageStats>& values)
|
||||
{
|
||||
output << header << "\n";
|
||||
std::vector<std::pair<std::string, StageStats>> sorted(values.begin(), values.end());
|
||||
std::sort(sorted.begin(), sorted.end(), [](const auto& left, const auto& right) {
|
||||
if (left.second.elapsed_us != right.second.elapsed_us)
|
||||
{
|
||||
return left.second.elapsed_us > right.second.elapsed_us;
|
||||
}
|
||||
|
||||
return left.first < right.first;
|
||||
});
|
||||
|
||||
for (const auto& [key, stats] : sorted)
|
||||
{
|
||||
output
|
||||
<< key
|
||||
<< "\tcount=" << stats.count
|
||||
<< "\tin=" << stats.input_bytes
|
||||
<< "\tout=" << stats.output_bytes
|
||||
<< "\ttime_ms=" << FormatMs(stats.elapsed_us)
|
||||
<< "\n";
|
||||
}
|
||||
output << "\n";
|
||||
}
|
||||
};
|
||||
|
||||
PackProfileState g_pack_profile;
|
||||
|
||||
bool ParseBoolFlag(std::string value, bool& out)
|
||||
{
|
||||
value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
}), value.end());
|
||||
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
|
||||
if (value == "1" || value == "true" || value == "yes" || value == "on")
|
||||
{
|
||||
out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value == "0" || value == "false" || value == "no" || value == "off")
|
||||
{
|
||||
out = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetEnvString(const char* name)
|
||||
{
|
||||
char buffer[256];
|
||||
const DWORD len = GetEnvironmentVariableA(name, buffer, sizeof(buffer));
|
||||
if (len == 0 || len >= sizeof(buffer))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::string(buffer, buffer + len);
|
||||
}
|
||||
|
||||
std::uint64_t CurrentElapsedUs()
|
||||
{
|
||||
if (!g_pack_profile.enabled)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
Clock::now() - g_pack_profile.start_time).count());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InitializePackProfile(const char* commandLine)
|
||||
{
|
||||
if (g_pack_profile.initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
g_pack_profile.initialized = true;
|
||||
g_pack_profile.enabled = false;
|
||||
|
||||
const std::string envProfile = GetEnvString(kPackProfileEnv);
|
||||
if (!envProfile.empty())
|
||||
{
|
||||
bool enabled = false;
|
||||
if (ParseBoolFlag(envProfile, enabled))
|
||||
{
|
||||
g_pack_profile.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
int argc = 0;
|
||||
PCHAR* argv = CommandLineToArgv(const_cast<PCHAR>(commandLine ? commandLine : ""), &argc);
|
||||
if (argv)
|
||||
{
|
||||
for (int i = 0; i < argc; ++i)
|
||||
{
|
||||
const std::string arg = argv[i];
|
||||
if (arg != "--m2pack-profile")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool enabled = true;
|
||||
if (i + 1 < argc)
|
||||
{
|
||||
const std::string next = argv[i + 1];
|
||||
if (!next.starts_with("--") && ParseBoolFlag(next, enabled))
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
g_pack_profile.enabled = enabled;
|
||||
}
|
||||
|
||||
SAFE_FREE_GLOBAL(argv);
|
||||
}
|
||||
|
||||
if (!g_pack_profile.enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
|
||||
g_pack_profile.current_phase = "startup";
|
||||
g_pack_profile.start_time = Clock::now();
|
||||
g_pack_profile.markers.push_back(PhaseMarker {
|
||||
.phase = g_pack_profile.current_phase,
|
||||
.elapsed_us = 0,
|
||||
});
|
||||
g_pack_profile.WriteReportLocked("initialized");
|
||||
g_pack_profile.snapshot_dirty = false;
|
||||
g_pack_profile.stop_snapshot_thread = false;
|
||||
g_pack_profile.StartSnapshotThread();
|
||||
}
|
||||
|
||||
bool IsPackProfileEnabled()
|
||||
{
|
||||
return g_pack_profile.enabled;
|
||||
}
|
||||
|
||||
void MarkPackProfilePhase(std::string_view phase)
|
||||
{
|
||||
if (!g_pack_profile.enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
|
||||
if (g_pack_profile.current_phase == phase)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
g_pack_profile.current_phase = std::string(phase);
|
||||
g_pack_profile.markers.push_back(PhaseMarker {
|
||||
.phase = g_pack_profile.current_phase,
|
||||
.elapsed_us = CurrentElapsedUs(),
|
||||
});
|
||||
g_pack_profile.WriteReportLocked(PackProfileState::MakePhaseKey("phase", phase));
|
||||
g_pack_profile.snapshot_dirty = false;
|
||||
}
|
||||
|
||||
void RecordPackProfileMount(
|
||||
std::string_view format,
|
||||
std::string_view packPath,
|
||||
bool ok,
|
||||
std::size_t entryCount,
|
||||
std::uint64_t elapsedUs)
|
||||
{
|
||||
if (!g_pack_profile.enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
|
||||
|
||||
auto& formatStats = g_pack_profile.mount_by_format[std::string(format)];
|
||||
formatStats.count += 1;
|
||||
formatStats.failures += ok ? 0 : 1;
|
||||
formatStats.entry_count += entryCount;
|
||||
formatStats.elapsed_us += elapsedUs;
|
||||
|
||||
auto& packStats = g_pack_profile.mount_by_pack[PackProfileState::ParsePackLabel(packPath)];
|
||||
packStats.count += 1;
|
||||
packStats.failures += ok ? 0 : 1;
|
||||
packStats.entry_count += entryCount;
|
||||
packStats.elapsed_us += elapsedUs;
|
||||
g_pack_profile.snapshot_dirty = true;
|
||||
}
|
||||
|
||||
void RecordPackProfileLoad(
|
||||
std::string_view format,
|
||||
std::string_view packPath,
|
||||
std::string_view requestPath,
|
||||
bool ok,
|
||||
std::uint64_t outputBytes,
|
||||
std::uint64_t elapsedUs)
|
||||
{
|
||||
if (!g_pack_profile.enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
|
||||
|
||||
auto update = [ok, outputBytes, elapsedUs](LoadStats& stats) {
|
||||
stats.count += 1;
|
||||
stats.failures += ok ? 0 : 1;
|
||||
stats.output_bytes += outputBytes;
|
||||
stats.elapsed_us += elapsedUs;
|
||||
};
|
||||
|
||||
update(g_pack_profile.load_by_format[std::string(format)]);
|
||||
update(g_pack_profile.load_by_phase_format[
|
||||
PackProfileState::MakePhaseKey(g_pack_profile.current_phase, format)]);
|
||||
update(g_pack_profile.load_by_phase_pack[
|
||||
PackProfileState::MakePhaseKey(g_pack_profile.current_phase, PackProfileState::ParsePackLabel(packPath))]);
|
||||
update(g_pack_profile.load_by_extension[PackProfileState::ParseExtension(requestPath)]);
|
||||
g_pack_profile.snapshot_dirty = true;
|
||||
}
|
||||
|
||||
void RecordPackProfileStage(
|
||||
std::string_view format,
|
||||
std::string_view stage,
|
||||
std::uint64_t inputBytes,
|
||||
std::uint64_t outputBytes,
|
||||
std::uint64_t elapsedUs)
|
||||
{
|
||||
if (!g_pack_profile.enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
|
||||
auto& stats = g_pack_profile.stage_by_key[
|
||||
PackProfileState::MakePhaseKey(format, stage)];
|
||||
stats.count += 1;
|
||||
stats.input_bytes += inputBytes;
|
||||
stats.output_bytes += outputBytes;
|
||||
stats.elapsed_us += elapsedUs;
|
||||
g_pack_profile.snapshot_dirty = true;
|
||||
}
|
||||
|
||||
void FlushPackProfileSnapshot(std::string_view reason)
|
||||
{
|
||||
if (!g_pack_profile.enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
|
||||
g_pack_profile.WriteReportLocked(reason);
|
||||
g_pack_profile.snapshot_dirty = false;
|
||||
}
|
||||
29
src/PackLib/PackProfile.h
Normal file
29
src/PackLib/PackProfile.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
void InitializePackProfile(const char* commandLine);
|
||||
bool IsPackProfileEnabled();
|
||||
void MarkPackProfilePhase(std::string_view phase);
|
||||
void RecordPackProfileMount(
|
||||
std::string_view format,
|
||||
std::string_view packPath,
|
||||
bool ok,
|
||||
std::size_t entryCount,
|
||||
std::uint64_t elapsedUs);
|
||||
void RecordPackProfileLoad(
|
||||
std::string_view format,
|
||||
std::string_view packPath,
|
||||
std::string_view requestPath,
|
||||
bool ok,
|
||||
std::uint64_t outputBytes,
|
||||
std::uint64_t elapsedUs);
|
||||
void RecordPackProfileStage(
|
||||
std::string_view format,
|
||||
std::string_view stage,
|
||||
std::uint64_t inputBytes,
|
||||
std::uint64_t outputBytes,
|
||||
std::uint64_t elapsedUs);
|
||||
void FlushPackProfileSnapshot(std::string_view reason);
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
#include "EterGrnLib/Material.h"
|
||||
|
||||
#include "resource.h"
|
||||
#include "GameType.h"
|
||||
#include "PythonApplication.h"
|
||||
#include "PythonCharacterManager.h"
|
||||
|
||||
#include "ProcessScanner.h"
|
||||
|
||||
#include <utf8.h>
|
||||
#include <cstdarg>
|
||||
#include <cstdlib>
|
||||
|
||||
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<int>(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<DWORD>(std::min(iIntervalMS, 60000));
|
||||
|
||||
if (m_bRenderTelemetryEnabled)
|
||||
{
|
||||
ResetRenderTelemetryTrace();
|
||||
AppendRenderTelemetryTrace("enabled interval_ms=%lu", static_cast<unsigned long>(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<DWORD>(1, dwNow - m_dwRenderTelemetryWindowStartMS);
|
||||
const double fWindowMS = static_cast<double>(dwWindowMS);
|
||||
const double fUpdateFPS = static_cast<double>(m_dwRenderTelemetryUpdateCount) * 1000.0 / fWindowMS;
|
||||
const double fRenderFPS = static_cast<double>(m_dwRenderTelemetryRenderCount) * 1000.0 / fWindowMS;
|
||||
const double fAvgUpdateMS = m_dwRenderTelemetryUpdateCount ? (m_fRenderTelemetryUpdateTimeSumMS / static_cast<double>(m_dwRenderTelemetryUpdateCount)) : 0.0;
|
||||
const double fAvgRenderMS = m_dwRenderTelemetryRenderCount ? (m_fRenderTelemetryRenderTimeSumMS / static_cast<double>(m_dwRenderTelemetryRenderCount)) : 0.0;
|
||||
const double fAvgSleepMS = m_dwRenderTelemetryLoopCount ? (m_fRenderTelemetrySleepTimeSumMS / static_cast<double>(m_dwRenderTelemetryLoopCount)) : 0.0;
|
||||
const double fAvgInterpolation = m_dwRenderTelemetryRenderCount ? (m_fRenderTelemetryInterpolationSum / static_cast<double>(m_dwRenderTelemetryRenderCount)) : 0.0;
|
||||
const double fAvgPresentGapMS = m_dwRenderTelemetryPresentGapSamples ? (m_fRenderTelemetryPresentGapSumMS / static_cast<double>(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<unsigned long>(dwElapsedMS),
|
||||
static_cast<unsigned long>(dwWindowMS),
|
||||
m_iFPS,
|
||||
fUpdateFPS,
|
||||
fRenderFPS,
|
||||
fAvgUpdateMS,
|
||||
fAvgRenderMS,
|
||||
fAvgSleepMS,
|
||||
fAvgPresentGapMS,
|
||||
fAvgInterpolation,
|
||||
static_cast<unsigned long>(m_dwRenderTelemetryLoopCount),
|
||||
static_cast<unsigned long>(m_dwRenderTelemetryUpdateCount),
|
||||
static_cast<unsigned long>(m_dwRenderTelemetryRenderCount),
|
||||
static_cast<unsigned long>(m_dwRenderTelemetryBlockedRenderCount),
|
||||
static_cast<unsigned long>(m_dwCurUpdateTime),
|
||||
static_cast<unsigned long>(m_dwCurRenderTime));
|
||||
}
|
||||
|
||||
ResetRenderTelemetryWindow(dwNow);
|
||||
}
|
||||
|
||||
bool CPythonApplication::IsRenderTelemetrySamplingEnabled() const
|
||||
{
|
||||
return m_bRenderTelemetryEnabled || m_bRenderTelemetryHudVisible;
|
||||
}
|
||||
|
||||
void CPythonApplication::RenderPerformanceHUD()
|
||||
{
|
||||
if (!m_bRenderTelemetryHudVisible)
|
||||
return;
|
||||
|
||||
CGraphicText* pkDefaultFont = static_cast<CGraphicText*>(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<double>(ELTimer_GetMSec());
|
||||
static double s_fNextRenderTime = static_cast<double>(ELTimer_GetMSec());
|
||||
static double s_fFixedFrameTime = static_cast<double>(ELTimer_GetMSec());
|
||||
|
||||
DWORD dwCurrentTime = ELTimer_GetMSec();
|
||||
const double fCurrentTime = static_cast<double>(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<DWORD>(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<double>(m_dwCurUpdateTime);
|
||||
}
|
||||
++s_dwUpdateFrameCount;
|
||||
}
|
||||
|
||||
const double fPreviousUpdateTime = s_fNextUpdateTime - kFixedUpdateMS;
|
||||
m_fRenderInterpolationFactor = ClampInterpolationFactor(static_cast<float>((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<double>(m_dwCurRenderTime);
|
||||
m_fRenderTelemetryInterpolationSum += static_cast<double>(m_fRenderInterpolationFactor);
|
||||
|
||||
if (m_dwRenderTelemetryLastPresentTime)
|
||||
{
|
||||
m_fRenderTelemetryPresentGapSumMS += static_cast<double>(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<double>(m_iFPS);
|
||||
s_fNextRenderTime += fRenderFrameMS;
|
||||
|
||||
++s_dwUpdateFrameCount;
|
||||
const double fSleepMS = s_fNextRenderTime - static_cast<double>(ELTimer_GetMSec());
|
||||
if (fSleepMS > 0.999)
|
||||
{
|
||||
const UINT uiSleepMS = static_cast<UINT>(fSleepMS);
|
||||
s_uiLoad -= std::min(s_uiLoad, uiSleepMS);
|
||||
const DWORD dwSleepStart = ELTimer_GetMSec();
|
||||
Sleep(static_cast<DWORD>(fSleepMS));
|
||||
if (bSampleRenderTelemetry)
|
||||
m_fRenderTelemetrySleepTimeSumMS += static_cast<double>(ELTimer_GetMSec() - dwSleepStart);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
s_fNextRenderTime = static_cast<double>(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<unsigned long>(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<unsigned long>(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();
|
||||
|
||||
@@ -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 <qedit.h>
|
||||
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;
|
||||
|
||||
@@ -358,6 +358,34 @@ struct FCharacterManagerCharacterInstanceDeform
|
||||
//pInstance->Update();
|
||||
}
|
||||
};
|
||||
struct FCharacterManagerCharacterInstanceApplyRenderInterpolation
|
||||
{
|
||||
explicit FCharacterManagerCharacterInstanceApplyRenderInterpolation(float fInterpolation)
|
||||
: m_fInterpolation(fInterpolation)
|
||||
{
|
||||
}
|
||||
|
||||
inline void operator () (const std::pair<DWORD, CInstanceBase*>& 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());
|
||||
|
||||
@@ -57,6 +57,7 @@ class CPythonCharacterManager : public CSingleton<CPythonCharacterManager>, publ
|
||||
void DestroyDeviceObjects();
|
||||
|
||||
void Update();
|
||||
void ApplyRenderInterpolation(float fInterpolation);
|
||||
void Deform();
|
||||
void Render();
|
||||
void RenderShadowMainInstance();
|
||||
|
||||
@@ -856,15 +856,14 @@ PyObject * chrGetProjectPosition(PyObject* poSelf, PyObject* poArgs)
|
||||
if (!pInstance)
|
||||
return Py_BuildValue("ii", -100, -100);
|
||||
|
||||
TPixelPosition PixelPosition;
|
||||
pInstance->NEW_GetPixelPosition(&PixelPosition);
|
||||
const D3DXVECTOR3& c_rv3Position = pInstance->GetGraphicThingInstanceRef().GetPosition();
|
||||
|
||||
CPythonGraphic & rpyGraphic = CPythonGraphic::Instance();
|
||||
|
||||
float fx, fy, fz;
|
||||
rpyGraphic.ProjectPosition(PixelPosition.x,
|
||||
-PixelPosition.y,
|
||||
PixelPosition.z + float(iHeight),
|
||||
rpyGraphic.ProjectPosition(c_rv3Position.x,
|
||||
c_rv3Position.y,
|
||||
c_rv3Position.z + float(iHeight),
|
||||
&fx, &fy, &fz);
|
||||
|
||||
if (1 == int(fz))
|
||||
|
||||
@@ -317,6 +317,7 @@ class CPythonNetworkStream : public CNetworkStream, public CSingleton<CPythonNet
|
||||
// Main Game Phase
|
||||
bool SendC2CPacket(DWORD dwSize, void * pData);
|
||||
bool SendChatPacket(const char * c_szChat, BYTE byType = CHAT_TYPE_TALKING);
|
||||
bool SendBiologSubmit();
|
||||
bool SendWhisperPacket(const char * name, const char * c_szChat);
|
||||
bool SendMessengerAddByVIDPacket(DWORD vid);
|
||||
bool SendMessengerAddByNamePacket(const char * c_szName);
|
||||
|
||||
@@ -499,6 +499,13 @@ PyObject* netSendChatPacket(PyObject* poSelf, PyObject* poArgs)
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* netSendBiologSubmit(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
CPythonNetworkStream& rkNetStream = CPythonNetworkStream::Instance();
|
||||
rkNetStream.SendBiologSubmit();
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* netSendEmoticon(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int eEmoticon;
|
||||
@@ -1700,6 +1707,7 @@ void initnet()
|
||||
{ "IsConnect", netIsConnect, METH_VARARGS },
|
||||
|
||||
{ "SendChatPacket", netSendChatPacket, METH_VARARGS },
|
||||
{ "SendBiologSubmit", netSendBiologSubmit, METH_VARARGS },
|
||||
{ "SendEmoticon", netSendEmoticon, METH_VARARGS },
|
||||
{ "SendWhisperPacket", netSendWhisperPacket, METH_VARARGS },
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "PythonApplication.h"
|
||||
|
||||
#include "GameLib/ItemManager.h"
|
||||
#include "PackLib/PackProfile.h"
|
||||
|
||||
#include "AbstractApplication.h"
|
||||
#include "AbstractCharacterManager.h"
|
||||
@@ -414,6 +415,7 @@ void CPythonNetworkStream::SetGamePhase()
|
||||
if ("Game"!=m_strPhase)
|
||||
m_phaseLeaveFunc.Run();
|
||||
|
||||
MarkPackProfilePhase("game");
|
||||
m_strPhase = "Game";
|
||||
|
||||
m_dwChangingPhaseTime = ELTimer_GetMSec();
|
||||
@@ -786,6 +788,11 @@ bool CPythonNetworkStream::SendChatPacket(const char * c_szChat, BYTE byType)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CPythonNetworkStream::SendBiologSubmit()
|
||||
{
|
||||
return SendChatPacket("/biolog_submit");
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Emoticon
|
||||
void CPythonNetworkStream::RegisterEmoticonString(const char * pcEmoticonString)
|
||||
@@ -4355,4 +4362,4 @@ void CPythonNetworkStream::Discord_Close()
|
||||
{
|
||||
Discord_Shutdown();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "NetworkActorManager.h"
|
||||
#include "AbstractPlayer.h"
|
||||
#include "PackLib/PackManager.h"
|
||||
#include "PackLib/PackProfile.h"
|
||||
|
||||
void CPythonNetworkStream::EnableChatInsultFilter(bool isEnable)
|
||||
{
|
||||
@@ -90,6 +91,7 @@ void CPythonNetworkStream::SetLoadingPhase()
|
||||
Tracen("");
|
||||
Tracen("## Network - Loading Phase ##");
|
||||
Tracen("");
|
||||
MarkPackProfilePhase("loading");
|
||||
|
||||
m_strPhase = "Loading";
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "PythonNetworkStream.h"
|
||||
#include "Packet.h"
|
||||
#include "AccountConnector.h"
|
||||
#include "PackLib/PackProfile.h"
|
||||
|
||||
// Login ---------------------------------------------------------------------------
|
||||
void CPythonNetworkStream::LoginPhase()
|
||||
@@ -18,6 +19,7 @@ void CPythonNetworkStream::SetLoginPhase()
|
||||
Tracen("");
|
||||
Tracen("## Network - Login Phase ##");
|
||||
Tracen("");
|
||||
MarkPackProfilePhase("login");
|
||||
|
||||
m_strPhase = "Login";
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -78,6 +78,8 @@ class CPythonSystem : public CSingleton<CPythonSystem>
|
||||
bool bAlwaysShowName;
|
||||
bool bShowDamage;
|
||||
bool bShowSalesText;
|
||||
int iRenderFPS;
|
||||
bool bShowPerformanceHUD;
|
||||
} TConfig;
|
||||
|
||||
public:
|
||||
@@ -157,6 +159,11 @@ class CPythonSystem : public CSingleton<CPythonSystem>
|
||||
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<CPythonSystem>
|
||||
bool m_isInterfaceConfig;
|
||||
PyObject * m_poInterfaceHandler;
|
||||
TWindowStatus m_WindowStatus[WINDOW_MAX_NUM];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
|
||||
|
||||
Reference in New Issue
Block a user