拆分时间源以稳定 IMX6U 固定步长主循环
将计时职责从显示后端中移出,新增 Platform::ITimeSource 和 Core::Timer,使主循环基于单调整数毫秒生成固定步长 tick。 这样 SDL2/fb0 只负责显示和输入,游戏逻辑不再通过 Display 获取时间,也避免用 float 秒作为核心时间源。 同时增加 30/45/60 FPS 命令行档位,并用整数余数累计生成 33/33/34、22/22/22/22/23、16/17/17 这类毫秒节奏,便于在 IMX6U 上按性能预算选择目标帧率。 Constraint: IMX6U 运行时应避免核心逻辑依赖 float deltaSeconds Constraint: SDL2 只能作为显示/输入适配层,不能扩散到核心时间逻辑 Rejected: 继续让 IDisplay::get_time_ms 提供时间 | 显示后端职责过宽,SDL/fb0 切换会影响时间语义 Rejected: 直接固定 33ms 实现 30 FPS | 长时间运行会产生节拍漂移 Confidence: high Scope-risk: narrow Directive: 后续 Launcher/GameA/GameB 应通过 ITimeSource + Timer 获取 fixed_delta_ms,不要重新引入 Display 计时 Tested: cmake --build build-win --config Release Not-tested: IMX6U 实机 clock_gettime(CLOCK_MONOTONIC) 帧时间稳定性
This commit is contained in:
parent
20d2422650
commit
777ff96602
24
README.md
24
README.md
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
- **Windows 编译**:开发主力机通常是 Windows,有 IDE 调试、有图形窗口,渲染算法对不对一眼就能看到。这个阶段完全不关心嵌入式细节。
|
||||
- **Linux x86 编译**:验证代码在 GCC/Clang 下有无警告、CMake 配置是否跨平台、系统 SDL2 依赖是否正确。很多嵌入式工具链的问题在 x86 Linux 上就能提前暴露。
|
||||
- **ARM 交叉编译**:最终在 IMX6U 上跑。若目标板使用 SDL2,则 SDL2 仅作为显示/输入/计时适配层,核心渲染仍按 CPU framebuffer + 一次性提交设计;如需极简依赖,也保留 `/dev/fb0` 后端作为对照。
|
||||
- **ARM 交叉编译**:最终在 IMX6U 上跑。若目标板使用 SDL2,则 SDL2 仅作为显示/输入适配层,时间由独立 `Platform::ITimeSource` 提供,核心渲染仍按 CPU framebuffer + 一次性提交设计;如需极简依赖,也保留 `/dev/fb0` 后端作为对照。
|
||||
|
||||
|
||||
## 开发规范与性能红线
|
||||
|
|
@ -28,14 +28,14 @@ IMX6U 运行时性能预算较紧,后续开发必须遵守 `docs/DEVELOPMENT_G
|
|||
- 核心逻辑和热路径默认不新增 `float` / `double`,需要小数时使用项目统一定点数;只在显示、调试、导入导出等边界层转换成浮点。
|
||||
- 主循环、每对象、每三角形/顶点/像素级代码中不得频繁创建 `std::vector` / `std::string` 等动态分配容器,应复用缓冲或使用固定容量结构。
|
||||
- PC/SDL 版本用于调试验证,不能把调试便利写法扩散到 ARM release 核心路径。
|
||||
- SDL2 后端只做显示、输入、计时和最终 framebuffer 提交,不在核心渲染/游戏逻辑中直接调用 SDL API。
|
||||
- SDL2 后端只做显示、输入和最终 framebuffer 提交;时间源独立于显示后端,不在核心渲染/游戏逻辑中直接调用 SDL API。
|
||||
- 新增核心代码前按规范文档中的检查清单自查。
|
||||
|
||||
## 应用层与图形库拆分
|
||||
|
||||
项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_GFX_ARCHITECTURE.md`:
|
||||
|
||||
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、输入/时间源、基础绘制能力,例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
|
||||
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力,例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
|
||||
- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。
|
||||
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
|
||||
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
|
||||
|
|
@ -66,6 +66,15 @@ cmake --build build-win --config Release
|
|||
./build-win/Release/IMX6U-Game.exe
|
||||
```
|
||||
|
||||
可选帧率档位:
|
||||
```bash
|
||||
./build-win/Release/IMX6U-Game.exe --fps 30
|
||||
./build-win/Release/IMX6U-Game.exe --fps 45
|
||||
./build-win/Release/IMX6U-Game.exe --fps 60
|
||||
```
|
||||
|
||||
当前只接受 `30`、`45`、`60` 三档。主循环从 `Platform::ITimeSource` 读取单调整数毫秒时间,由 `Core::Timer` 生成固定步长 tick,并通过 `33/33/34`、`22/22/22/22/23`、`16/17/17` 这类整数节奏逼近对应目标帧率,避免核心时间源依赖 `float` 秒。
|
||||
|
||||
### Linux x86_64(Ubuntu / WSL2)
|
||||
|
||||
需要系统 SDL2:
|
||||
|
|
@ -95,7 +104,7 @@ sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
|
|||
|
||||
本项目 ARM 端保留两类后端:
|
||||
|
||||
- **SDL2 后端**:目标是后续游戏主路径;SDL2 负责显示、输入、计时和最终 framebuffer 提交。
|
||||
- **SDL2 后端**:目标是后续游戏主路径;SDL2 负责显示、输入和最终 framebuffer 提交,时间源使用独立的 `Platform::ITimeSource`。
|
||||
- **Framebuffer 后端**:作为极简依赖和显示通路对照测试。
|
||||
|
||||
构建(Framebuffer 对照后端):
|
||||
|
|
@ -196,7 +205,8 @@ assets/font/font_atlas.h
|
|||
│ Gfx:DrawContext、FrameBuffer、Draw2D、Math │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ Platform::IDisplay │
|
||||
│ - SDLDisplay : SDL2 显示/输入/计时适配 │
|
||||
│ - SDLDisplay : SDL2 显示/输入适配 │
|
||||
│ - ITimeSource: 单调整数毫秒时间源 │
|
||||
│ - FBDisplay : /dev/fb0 对照后端 │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
|
@ -228,7 +238,7 @@ IMX6U-Game/
|
|||
│ │ ├─ RenderData/ # Color、Triangle 等数据结构
|
||||
│ │ ├─ Scene/ # Camera、Transform、Mesh
|
||||
│ │ ├─ Shading/ # 着色器(预留)
|
||||
│ │ ├─ Platform/ # IDisplay、SDLDisplay、FBDisplay
|
||||
│ │ ├─ Platform/ # IDisplay、SDLDisplay、FBDisplay、ITimeSource
|
||||
│ │ └─ Asset/ # ObjLoader 等资源加载
|
||||
│ ├─ Apps/
|
||||
│ │ └─ Demo/ # 当前 3D 立方体 demo 入口
|
||||
|
|
@ -251,6 +261,7 @@ IMX6U-Game/
|
|||
### Core
|
||||
- **FrameBuffer**:CPU 侧颜色缓冲,渲染结果先写在这里
|
||||
- **DepthBuffer**:深度测试用 Z-buffer
|
||||
- **Timer**:整数毫秒固定步长 tick 生成器,支持 30/45/60 FPS 档位和每帧剩余时间计算
|
||||
|
||||
### Math
|
||||
- 通用数学类型:`Vector2/3/4`、`Matrix4x4`
|
||||
|
|
@ -264,6 +275,7 @@ IMX6U-Game/
|
|||
- **IDisplay**:显示后端抽象,解耦渲染与输出
|
||||
- **SDLDisplay**:SDL2 后端,PC 调试和 IMX6U SDL2 目标路径共用这一类适配思想
|
||||
- **FBDisplay**:`/dev/fb0` 对照后端,用于极简显示通路验证
|
||||
- **ITimeSource / SteadyTimeSource**:独立时间源接口与单调时钟实现;Linux/IMX6U 使用 `clock_gettime(CLOCK_MONOTONIC)`,Windows 使用 `std::chrono::steady_clock`,Display 不再承担计时职责
|
||||
|
||||
## 当前状态与后续
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ IMX6U-Game/
|
|||
│ │ ├─ RenderData/ # Color、Triangle 等数据结构(✅ 已实现)
|
||||
│ │ ├─ Scene/ # Camera、Transform(✅ 已实现)
|
||||
│ │ ├─ Shading/ # 着色器(预留)
|
||||
│ │ ├─ Platform/ # SDL2 / fb0 平台适配(✅ 已实现)
|
||||
│ │ ├─ Platform/ # SDL2 / fb0 显示适配与独立时间源(✅ 已实现)
|
||||
│ │ └─ Asset/ # 资源加载(✅ 已实现)
|
||||
│ ├─ Apps/
|
||||
│ │ ├─ Demo/ # 当前 3D 立方体 demo(✅ 已实现)
|
||||
|
|
@ -54,7 +54,7 @@ IMX6U-Game/
|
|||
- 管理 framebuffer、depthbuffer、渲染上下文。
|
||||
- 提供基础绘制接口:点、线、矩形、四边形、三角形、sprite、SpriteRegion、tilemap、简单文本等。
|
||||
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
|
||||
- 封装 SDL2 / framebuffer 显示提交、输入轮询、时间源。
|
||||
- 封装 SDL2 / framebuffer 显示提交、输入轮询,并通过独立 `ITimeSource` 提供单调整数毫秒时间。
|
||||
- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas),运行时不直接依赖 PNG/TTF 解码。
|
||||
- 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ Shared 可以依赖 Gfx;Gfx 不能依赖 Shared。
|
|||
|
||||
```text
|
||||
Apps/GameA ─┐
|
||||
Apps/GameB ─┼─> Shared ─> Gfx ─> Platform(SDL2/fb0)
|
||||
Apps/GameB ─┼─> Shared ─> Gfx ─> Platform(SDL2/fb0/ITimeSource)
|
||||
Launcher ─┘
|
||||
|
||||
Apps/GameA ─┐
|
||||
|
|
@ -160,7 +160,7 @@ public:
|
|||
poll input -> update current app -> render current app -> present framebuffer
|
||||
```
|
||||
|
||||
这样三个应用层共用同一个主循环、同一套 SDL2 初始化和 framebuffer 提交流程。
|
||||
这样三个应用层共用同一个主循环、同一套 SDL2 初始化、framebuffer 提交流程和独立时间源。
|
||||
|
||||
注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 `draw_rect`、`draw_quad`、`set_pixel_fast` 这类热路径函数不要虚化。
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ namespace Gfx
|
|||
## 8. 性能注意事项
|
||||
|
||||
- 应用切换不应重复销毁/创建 SDL window、renderer、texture。
|
||||
- 三个应用共用 framebuffer、输入状态和时间源。
|
||||
- 三个应用共用 framebuffer、输入状态和 `Platform::ITimeSource` 时间源;Display 不承担计时职责。
|
||||
- 每个应用可以有自己的资源缓存,但必须有上限和释放策略。
|
||||
- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
|
||||
- Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。
|
||||
|
|
|
|||
|
|
@ -159,14 +159,14 @@
|
|||
|
||||
## 12. IMX6U + SDL2 运行后端规范
|
||||
|
||||
如果最终版本在 IMX6U 上使用 SDL2,而不是直接写 framebuffer,需要额外注意:SDL2 只是显示、输入、计时和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
|
||||
如果最终版本在 IMX6U 上使用 SDL2,而不是直接写 framebuffer,需要额外注意:SDL2 只是显示、输入和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源;时间源应通过独立 `Platform::ITimeSource` 提供。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
|
||||
|
||||
### 12.1 SDL2 边界
|
||||
|
||||
- SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。
|
||||
- `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/Draw2D` 不应直接包含 `SDL.h`。
|
||||
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
|
||||
- 时间源可以来自 SDL,但核心逻辑使用整数 tick / fixed timestep,不直接依赖 float 秒数。
|
||||
- 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep,不直接依赖 float 秒数。
|
||||
|
||||
### 12.2 提交帧策略
|
||||
|
||||
|
|
@ -188,7 +188,8 @@
|
|||
|
||||
- 先确定目标分辨率和目标 FPS,再决定渲染功能;不要默认使用屏幕原生高分辨率。
|
||||
- IMX6U 上优先考虑低分辨率内部渲染,再由 SDL/显示层整数倍放大。
|
||||
- 目标建议至少维护两个档位:开发调试档、IMX6U 性能档。
|
||||
- 目标帧率当前维护 `30/45/60 FPS` 三档,通过命令行 `--fps 30|45|60` 选择;IMX6U 默认优先 30 FPS,45/60 FPS 需要以实测帧时间确认。
|
||||
- 固定步长 tick 使用整数毫秒余数累计,例如 30 FPS 为 `33/33/34 ms` 节奏,45 FPS 为 `22/22/22/22/23 ms` 节奏,60 FPS 为 `16/17/17 ms` 节奏。
|
||||
- 性能档应限制:最大三角形数、最大 sprite 数、最大粒子数、最大动态光源数、最大纹理尺寸。
|
||||
- 所有预算都应以实测帧时间为准,不能只按 PC 表现推断。
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include "Vector2.h"
|
||||
#include "Vector3.h"
|
||||
#include "Vector4.h"
|
||||
|
|
@ -11,12 +14,14 @@
|
|||
#include "Triangle.h"
|
||||
#include "Camera.h"
|
||||
#include <cstdlib>
|
||||
#include "Timer.h"
|
||||
#include "Vertex.h"
|
||||
#include "DrawContext.h"
|
||||
#include "test_sprite.h"
|
||||
#include "font_atlas.h"
|
||||
|
||||
#include "Display.h"
|
||||
#include "TimeSource.h"
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
#include "FBDisplay.h"
|
||||
#else
|
||||
|
|
@ -103,8 +108,93 @@ static bool IsTriangleVisible(const CubeTriangle &triangle, const std::array<Mat
|
|||
return faceNormal.dot(faceCenter) > 0.0f;
|
||||
}
|
||||
|
||||
static void PrintUsage(const char *program_name)
|
||||
{
|
||||
std::cout
|
||||
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
|
||||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
struct ProgramOptions
|
||||
{
|
||||
uint32_t target_fps;
|
||||
bool show_help;
|
||||
|
||||
ProgramOptions()
|
||||
: target_fps(Core::Timer::DefaultFps),
|
||||
show_help(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
|
||||
{
|
||||
ProgramOptions options;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
const char *arg = argv[i];
|
||||
const char *fps_value = nullptr;
|
||||
|
||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||
{
|
||||
options.show_help = true;
|
||||
return options;
|
||||
}
|
||||
else if (std::strcmp(arg, "--fps") == 0)
|
||||
{
|
||||
if (i + 1 < argc)
|
||||
{
|
||||
fps_value = argv[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
|
||||
}
|
||||
}
|
||||
else if (std::strncmp(arg, "--fps=", 6) == 0)
|
||||
{
|
||||
fps_value = arg + 6;
|
||||
}
|
||||
|
||||
if (fps_value != nullptr)
|
||||
{
|
||||
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
|
||||
if (Core::Timer::is_supported_fps(parsed_fps))
|
||||
{
|
||||
options.target_fps = parsed_fps;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
|
||||
options.target_fps = Core::Timer::DefaultFps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::ITimeSource &time_source)
|
||||
{
|
||||
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
|
||||
if (sleep_ms > 0u)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
||||
if (options.show_help)
|
||||
{
|
||||
PrintUsage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
Core::Timer timer(options.target_fps);
|
||||
Platform::SteadyTimeSource time_source;
|
||||
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
Platform::IDisplay *display = new Platform::FBDisplay();
|
||||
#else
|
||||
|
|
@ -118,6 +208,7 @@ int main(int argc, char *argv[])
|
|||
}
|
||||
|
||||
Gfx::DrawContext ctx(width, height);
|
||||
std::cout << "Target FPS: " << timer.target_fps() << std::endl;
|
||||
|
||||
RenderData::BitmapFont font;
|
||||
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
|
||||
|
|
@ -177,16 +268,21 @@ int main(int argc, char *argv[])
|
|||
char fps_text[32];
|
||||
|
||||
bool isRunning = true;
|
||||
uint32_t animation_time_ms = 0;
|
||||
while (isRunning)
|
||||
{
|
||||
timer.begin_frame(time_source.get_time_ms());
|
||||
const uint32_t fixed_delta_ms = timer.fixed_delta_ms();
|
||||
animation_time_ms += fixed_delta_ms;
|
||||
|
||||
display->poll_events(isRunning);
|
||||
|
||||
ctx.clear(clearColor);
|
||||
|
||||
const float timeSeconds = static_cast<float>(display->get_time_ms()) * 0.001f;
|
||||
const float animation_time = static_cast<float>(animation_time_ms) / 1000.0f;
|
||||
const Math::Matrix4x4 model =
|
||||
Math::MathUtil::get_rotation_matrix_y(timeSeconds) *
|
||||
Math::MathUtil::get_rotation_matrix_x(timeSeconds * 0.6f);
|
||||
Math::MathUtil::get_rotation_matrix_y(animation_time) *
|
||||
Math::MathUtil::get_rotation_matrix_x(static_cast<float>(animation_time_ms * 6u) / 10000.0f);
|
||||
const Math::Matrix4x4 view = camera.get_view_matrix();
|
||||
const Math::Matrix4x4 modelView = view * model;
|
||||
const Math::Matrix4x4 projection = camera.get_perspective_projection_matrix(aspectRatio);
|
||||
|
|
@ -266,11 +362,11 @@ int main(int argc, char *argv[])
|
|||
ctx.draw_sprite(10, 10, sprite_img);
|
||||
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
|
||||
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
|
||||
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(display->get_time_ms() / 20) % 32, 0);
|
||||
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(animation_time_ms / 20u) % 32, 0);
|
||||
|
||||
// FPS 计数
|
||||
++frame_count;
|
||||
const uint32_t now = display->get_time_ms();
|
||||
const uint32_t now = time_source.get_time_ms();
|
||||
if (now - last_fps_time >= 1000)
|
||||
{
|
||||
fps = frame_count;
|
||||
|
|
@ -281,6 +377,7 @@ int main(int argc, char *argv[])
|
|||
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
|
||||
|
||||
ctx.present(display);
|
||||
SleepRemainingFrameTime(timer, time_source);
|
||||
}
|
||||
|
||||
display->shutdown();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class Timer
|
||||
{
|
||||
public:
|
||||
static const uint32_t DefaultFps = 30;
|
||||
|
||||
explicit Timer(uint32_t target_fps = DefaultFps)
|
||||
: target_fps_(normalize_fps(target_fps)),
|
||||
tick_remainder_(0),
|
||||
frame_start_ms_(0),
|
||||
fixed_delta_ms_(0)
|
||||
{
|
||||
}
|
||||
|
||||
void begin_frame(uint32_t now_ms)
|
||||
{
|
||||
frame_start_ms_ = now_ms;
|
||||
fixed_delta_ms_ = next_tick_ms();
|
||||
}
|
||||
|
||||
uint32_t target_fps() const
|
||||
{
|
||||
return target_fps_;
|
||||
}
|
||||
|
||||
uint32_t fixed_delta_ms() const
|
||||
{
|
||||
return fixed_delta_ms_;
|
||||
}
|
||||
|
||||
uint32_t frame_start_ms() const
|
||||
{
|
||||
return frame_start_ms_;
|
||||
}
|
||||
|
||||
uint32_t remaining_frame_ms(uint32_t now_ms) const
|
||||
{
|
||||
const uint32_t elapsed_ms = now_ms - frame_start_ms_;
|
||||
return elapsed_ms < fixed_delta_ms_ ? fixed_delta_ms_ - elapsed_ms : 0u;
|
||||
}
|
||||
|
||||
static bool is_supported_fps(uint32_t fps)
|
||||
{
|
||||
return fps == 30u || fps == 45u || fps == 60u;
|
||||
}
|
||||
|
||||
static uint32_t normalize_fps(uint32_t fps)
|
||||
{
|
||||
return is_supported_fps(fps) ? fps : DefaultFps;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t next_tick_ms()
|
||||
{
|
||||
tick_remainder_ += 1000u;
|
||||
const uint32_t tick_ms = tick_remainder_ / target_fps_;
|
||||
tick_remainder_ %= target_fps_;
|
||||
return tick_ms;
|
||||
}
|
||||
|
||||
uint32_t target_fps_;
|
||||
uint32_t tick_remainder_;
|
||||
uint32_t frame_start_ms_;
|
||||
uint32_t fixed_delta_ms_;
|
||||
};
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ namespace Platform
|
|||
virtual bool init(int width, int height) = 0;
|
||||
virtual void present(const Core::FrameBuffer* framebuffer) = 0;
|
||||
virtual void poll_events(bool& should_quit) = 0;
|
||||
virtual uint32_t get_time_ms() const = 0;
|
||||
virtual void shutdown() = 0;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,13 +119,6 @@ namespace Platform
|
|||
}
|
||||
}
|
||||
|
||||
uint32_t FBDisplay::get_time_ms() const
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return static_cast<uint32_t>(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
||||
}
|
||||
|
||||
void FBDisplay::shutdown()
|
||||
{
|
||||
if (fb_mem != nullptr)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ namespace Platform
|
|||
bool init(int w, int h) override;
|
||||
void present(const Core::FrameBuffer* framebuffer) override;
|
||||
void poll_events(bool& should_quit) override;
|
||||
uint32_t get_time_ms() const override;
|
||||
void shutdown() override;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,11 +76,6 @@ namespace Platform
|
|||
}
|
||||
}
|
||||
|
||||
uint32_t SDLDisplay::get_time_ms() const
|
||||
{
|
||||
return SDL_GetTicks();
|
||||
}
|
||||
|
||||
void SDLDisplay::shutdown()
|
||||
{
|
||||
if (texture != nullptr)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace Platform
|
|||
bool init(int w, int h) override;
|
||||
void present(const Core::FrameBuffer* framebuffer) override;
|
||||
void poll_events(bool& should_quit) override;
|
||||
uint32_t get_time_ms() const override;
|
||||
void shutdown() override;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <chrono>
|
||||
#else
|
||||
#include <ctime>
|
||||
#endif
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class ITimeSource
|
||||
{
|
||||
public:
|
||||
virtual ~ITimeSource() {}
|
||||
virtual uint32_t get_time_ms() const = 0;
|
||||
};
|
||||
|
||||
class SteadyTimeSource : public ITimeSource
|
||||
{
|
||||
public:
|
||||
SteadyTimeSource()
|
||||
:
|
||||
#ifdef _WIN32
|
||||
start_(std::chrono::steady_clock::now())
|
||||
#else
|
||||
start_ms_(read_monotonic_ms())
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
uint32_t get_time_ms() const override
|
||||
{
|
||||
#ifdef _WIN32
|
||||
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
const std::chrono::milliseconds elapsed =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - start_);
|
||||
return static_cast<uint32_t>(elapsed.count());
|
||||
#else
|
||||
return read_monotonic_ms() - start_ms_;
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
#ifdef _WIN32
|
||||
std::chrono::steady_clock::time_point start_;
|
||||
#else
|
||||
static uint32_t read_monotonic_ms()
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return static_cast<uint32_t>(ts.tv_sec * 1000u + ts.tv_nsec / 1000000u);
|
||||
}
|
||||
|
||||
uint32_t start_ms_;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue