拆分时间源以稳定 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:
SepComet
2026-06-07 10:09:07 +08:00
parent 20d2422650
commit 777ff96602
11 changed files with 259 additions and 34 deletions
+102 -5
View File
@@ -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();
+71
View File
@@ -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_;
};
}
-1
View File
@@ -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;
};
}
-7
View File
@@ -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)
-1
View File
@@ -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;
};
}
-5
View File
@@ -76,11 +76,6 @@ namespace Platform
}
}
uint32_t SDLDisplay::get_time_ms() const
{
return SDL_GetTicks();
}
void SDLDisplay::shutdown()
{
if (texture != nullptr)
-1
View File
@@ -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;
};
}
+59
View File
@@ -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
};
}