From 5c746d99c2b6ee05dfb556e4bfd9afa788d13c99 Mon Sep 17 00:00:00 2001 From: shweet Date: Thu, 7 May 2026 00:19:53 -0400 Subject: [PATCH] 60Hz --- src/imgui/dockspace.cpp | 4 +-- src/imgui/dockspace.hpp | 2 +- src/imgui/window/animation_preview.cpp | 41 ++++++++------------------ src/imgui/window/animation_preview.hpp | 4 +-- src/imgui/window/timeline.cpp | 2 +- src/imgui/wizard/configure.cpp | 7 +++++ src/playback.cpp | 33 ++++++++++----------- src/render.cpp | 5 ++-- src/resource/strings.hpp | 1 + src/settings.hpp | 1 + src/state.cpp | 18 +++-------- src/state.hpp | 2 +- 12 files changed, 51 insertions(+), 69 deletions(-) diff --git a/src/imgui/dockspace.cpp b/src/imgui/dockspace.cpp index fe81350..277afdf 100644 --- a/src/imgui/dockspace.cpp +++ b/src/imgui/dockspace.cpp @@ -2,10 +2,10 @@ namespace anm2ed::imgui { - void Dockspace::tick(Manager& manager, Settings& settings) + void Dockspace::tick(Manager& manager, Settings& settings, float deltaSeconds) { if (auto document = manager.get(); document) - if (settings.windowIsAnimationPreview) animationPreview.tick(manager, settings); + if (settings.windowIsAnimationPreview) animationPreview.tick(manager, settings, deltaSeconds); } void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings, diff --git a/src/imgui/dockspace.hpp b/src/imgui/dockspace.hpp index 8bdfd90..3d4c6e1 100644 --- a/src/imgui/dockspace.hpp +++ b/src/imgui/dockspace.hpp @@ -37,7 +37,7 @@ namespace anm2ed::imgui Welcome welcome; public: - void tick(Manager&, Settings&); + void tick(Manager&, Settings&, float); void update(Taskbar&, Documents&, Manager&, Settings&, Resources&, Dialog&, Clipboard&); }; } diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index 5bb262b..9096773 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -34,8 +34,6 @@ namespace anm2ed::imgui constexpr auto POINT_SIZE = vec2(4, 4); constexpr auto TRIGGER_TEXT_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); constexpr auto TRIGGER_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.5f); - constexpr auto PLAYBACK_TICK_RATE = 30.0f; - namespace { std::filesystem::path render_destination_directory(const std::filesystem::path& path, int type) @@ -153,7 +151,7 @@ namespace anm2ed::imgui AnimationPreview::AnimationPreview() : Canvas(vec2()) {} - void AnimationPreview::tick(Manager& manager, Settings& settings) + void AnimationPreview::tick(Manager& manager, Settings& settings, float deltaSeconds) { auto& document = *manager.get(); auto& anm2 = document.anm2; @@ -176,8 +174,9 @@ namespace anm2ed::imgui auto& path = settings.renderPath; auto pathString = path::to_utf8(path); auto& type = settings.renderType; + auto renderFrameRate = std::max(settings.playbackTickRate, 1); - if (playback.time > end || playback.isFinished) + if (playback.time >= (float)end + 1.0f || playback.isFinished) { if (settings.timelineIsSound) audioStream.capture_end(mixer); @@ -262,7 +261,7 @@ namespace anm2ed::imgui { if (settings.timelineIsSound && type != render::GIF) { - if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, anm2.info.fps)) + if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, renderFrameRate)) { toasts.push(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED)); logger.error("Failed to generate deterministic render audio stream; exporting without audio."); @@ -273,7 +272,7 @@ namespace anm2ed::imgui audioStream.stream.clear(); if (animation_render(ffmpegPath, path, renderTempFrames, renderTempFrameDurations, audioStream, - (render::Type)type, anm2.info.fps)) + (render::Type)type, renderFrameRate)) { toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString))); logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH), @@ -325,8 +324,10 @@ namespace anm2ed::imgui auto frameSoundID = -1; if (settings.timelineIsSound && !anm2.content.sounds.empty()) { - if (auto animation = document.animation_get(); - animation && animation->triggers.isVisible && (!settings.timelineIsOnlyShowLayers || manager.isRecording)) + auto soundTime = (int)std::floor(playback.time); + if (auto animation = document.animation_get(); soundTime != renderFrameSoundTimePrev && animation && + animation->triggers.isVisible && + (!settings.timelineIsOnlyShowLayers || manager.isRecording)) { if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible) { @@ -341,6 +342,7 @@ namespace anm2ed::imgui } } } + renderFrameSoundTimePrev = soundTime; } renderFrameSoundIDs.push_back(frameSoundID); @@ -352,24 +354,8 @@ namespace anm2ed::imgui if (Texture::write_pixels_png(framePath, size, pixels.data())) { renderTempFrames.push_back(framePath); - auto nowCounter = SDL_GetPerformanceCounter(); - auto counterFrequency = SDL_GetPerformanceFrequency(); - auto fallbackDuration = 1.0 / (double)std::max(anm2.info.fps, 1); - - if (renderTempFrames.size() == 1) - { - renderCaptureCounterPrev = nowCounter; - renderTempFrameDurations.push_back(fallbackDuration); - } - else - { - auto elapsedCounter = nowCounter - renderCaptureCounterPrev; - auto frameDuration = counterFrequency > 0 ? (double)elapsedCounter / (double)counterFrequency : 0.0; - frameDuration = std::max(frameDuration, 1.0 / 1000.0); - renderTempFrameDurations.back() = frameDuration; - renderTempFrameDurations.push_back(frameDuration); - renderCaptureCounterPrev = nowCounter; - } + auto fallbackDuration = 1.0 / (double)renderFrameRate; + renderTempFrameDurations.push_back(fallbackDuration); } else { @@ -416,7 +402,6 @@ namespace anm2ed::imgui } auto fps = std::max(anm2.info.fps, 1); - auto deltaSeconds = manager.isRecording ? (1.0f / (float)fps) : (1.0f / PLAYBACK_TICK_RATE); playback.tick(fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording, deltaSeconds); @@ -657,7 +642,7 @@ namespace anm2ed::imgui renderTempFrames.clear(); renderTempFrameDurations.clear(); renderFrameSoundIDs.clear(); - renderCaptureCounterPrev = 0; + renderFrameSoundTimePrev = -1; if (settings.renderType == render::PNGS) { renderTempDirectory = settings.renderPath; diff --git a/src/imgui/window/animation_preview.hpp b/src/imgui/window/animation_preview.hpp index b9dcd7a..3b591e2 100644 --- a/src/imgui/window/animation_preview.hpp +++ b/src/imgui/window/animation_preview.hpp @@ -33,11 +33,11 @@ namespace anm2ed::imgui std::vector renderTempFrames{}; std::vector renderTempFrameDurations{}; std::vector renderFrameSoundIDs{}; - Uint64 renderCaptureCounterPrev{}; + int renderFrameSoundTimePrev{-1}; public: AnimationPreview(); - void tick(Manager&, Settings&); + void tick(Manager&, Settings&, float); void update(Manager&, Settings&, Resources&); }; } diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index d8a0e06..f0b5599 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -1312,7 +1312,7 @@ namespace anm2ed::imgui document.frameTime = playback.time; } - playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX); + if (!playback.isPlaying) playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX); if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) isDragging = false; diff --git a/src/imgui/wizard/configure.cpp b/src/imgui/wizard/configure.cpp index 108b886..7b3b996 100644 --- a/src/imgui/wizard/configure.cpp +++ b/src/imgui/wizard/configure.cpp @@ -32,6 +32,13 @@ namespace anm2ed::imgui::wizard ImGui::Checkbox(localize.get(LABEL_VSYNC), &temporary.isVsync); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC)); + ImGui::SeparatorText(localize.get(LABEL_PLAYBACK_MENU)); + ImGui::RadioButton("30 Hz", &temporary.playbackTickRate, 30); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PLAYBACK_TICK_RATE)); + ImGui::SameLine(); + ImGui::RadioButton("60 Hz", &temporary.playbackTickRate, 60); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PLAYBACK_TICK_RATE)); + ImGui::SeparatorText(localize.get(LABEL_LOCALIZATION)); ImGui::Combo(localize.get(LABEL_LANGUAGE), &temporary.language, LANGUAGE_STRINGS, LANGUAGE_COUNT); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LANGUAGE)); diff --git a/src/playback.cpp b/src/playback.cpp index 1b6508e..36486b0 100644 --- a/src/playback.cpp +++ b/src/playback.cpp @@ -21,30 +21,27 @@ namespace anm2ed void Playback::tick(int fps, int length, bool isLoop, float deltaSeconds) { + if (isLoop) isFinished = false; if (isFinished || !isPlaying || fps <= 0 || length <= 0) return; if (deltaSeconds <= 0.0f) return; - auto frameDuration = 1.0f / (float)fps; - tickAccumulator += deltaSeconds; - auto steps = (int)std::floor(tickAccumulator / frameDuration); - if (steps <= 0) return; - tickAccumulator -= frameDuration * (float)steps; + time += deltaSeconds * (float)fps; - time += (float)steps; + if (!std::isfinite(time)) time = 0.0f; - if (time > (float)length - 1.0f) + if (isLoop) { - if (isLoop) - { - time = std::fmod(time, (float)length); - } - else - { - time = (float)length - 1.0f; - isPlaying = false; - isFinished = true; - timing_reset(); - } + time = std::fmod(time, (float)length); + if (time < 0.0f) time += (float)length; + return; + } + + if (time >= (float)length) + { + time = (float)length - 1.0f; + isPlaying = false; + isFinished = true; + timing_reset(); } } diff --git a/src/render.cpp b/src/render.cpp index 702d98e..4e32b2b 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -39,7 +39,6 @@ namespace anm2ed const std::vector& frameDurations, AudioStream& audioStream, render::Type type, int fps) { if (framePaths.empty() || ffmpegPath.empty() || path.empty()) return false; - (void)frameDurations; fps = std::max(fps, 1); auto pathString = path::to_utf8(path); @@ -132,6 +131,7 @@ namespace anm2ed auto framePath = framePaths[index]; auto framePathString = path::to_utf8(framePath); auto frameDuration = defaultFrameDuration; + if (index < frameDurations.size() && frameDurations[index] > 0.0) frameDuration = frameDurations[index]; framesListFile << "file '" << ffmpeg_concat_escape(framePathString) << "'\n"; framesListFile << "duration " << std::format("{:.9f}", frameDuration) << "\n"; } @@ -143,11 +143,12 @@ namespace anm2ed command = std::format("\"{0}\" -y -f concat -safe 0 -i \"{1}\"", ffmpegPathString, framesListPathString); if (!audioInputArguments.empty()) command += " " + audioInputArguments; - command += std::format(" -fps_mode cfr -r {}", fps); + if (type != render::GIF) command += std::format(" -fps_mode cfr -r {}", fps); switch (type) { case render::GIF: + command += " -fps_mode vfr"; command += " -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full:reserve_transparent=1[p];" "[s1][p]paletteuse=dither=floyd_steinberg:alpha_threshold=128\"" diff --git a/src/resource/strings.hpp b/src/resource/strings.hpp index c46ac2e..baeb289 100644 --- a/src/resource/strings.hpp +++ b/src/resource/strings.hpp @@ -606,6 +606,7 @@ namespace anm2ed X(TOOLTIP_PIVOTS, "Toggle the visibility of the animation's pivots.", "Alterna la visibilidad de los pivotes de la animacion.", "Переключить видимость точек поворота анимации.", "切换动画枢轴是否可见.", "애니메이션의 중심점을 표시하거나 숨깁니다.") \ X(TOOLTIP_PLAYBACK_ALWAYS_LOOP, "Animations will always loop during playback, even if looping isn't set.", "Las animaciones siempre se loopearan durante la reproduccion, incluso si \"looping\" no esta activado.", "Анимации всегда будут циклично воспроизводиться во время воспроизведения, даже если цикличность не установлена.", "动画预放时也会循环, 即使循环并未被设置.", "반복 설정이 켜져 있지 않아도 애니메이션이 항상 반복 재생됩니다.") \ X(TOOLTIP_PLAYBACK_CLAMP, "Operations will always be clamped to within the animation's bounds.\nFor example, dragging the playhead, or triggers.", "Las operaciones siempre se haran Clamp a las normas de la animacion.\nPor ejemplo: Arrastrar el Cabezal, o los triggers.", "Операции всегда будут ограничены границами анимации.\nНапример, перетаскивание воспроизводящей головки или триггеров.", "任何操作都会被限制在动画的区间内.\n比如, 拖拽播放指针或者触发器.", "작업이 항상 애니메이션 범위 내로 제한됩니다.\n예: 플레이헤드를 드래그하거나 트리거를 조작할 때.") \ + X(TOOLTIP_PLAYBACK_TICK_RATE, "Set how often animation playback updates while previewing.", "Ajusta con que frecuencia se actualiza la reproduccion de animacion durante la vista previa.", "Установить, как часто обновляется воспроизведение анимации в предпросмотре.", "设置动画预览播放时的更新频率。", "미리보기 중 애니메이션 재생이 업데이트되는 빈도를 설정합니다.") \ X(TOOLTIP_PLAY_ANIMATION, "Play the animation.", "Reproduce la animacion.", "Возпроизвести анимацию.", "播放动画.", "애니메이션을 재생합니다.") \ X(TOOLTIP_POSITION, "Change the position of the frame.", "Cambia la posicion del Frame.", "Изменить позицию кадра.", "更改此帧的位置.", "프레임의 위치를 변경합니다.") \ X(TOOLTIP_PREVIEW_ZOOM, "Change the zoom of the preview.", "Cambia el zoom de la vista previa.", "Изменить масштаб предпросмотра.", "更改预览视图的缩放.", "미리보기의 줌을 변경합니다.") \ diff --git a/src/settings.hpp b/src/settings.hpp index 8de04c9..1f47fec 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -74,6 +74,7 @@ namespace anm2ed \ X(PLAYBACK_IS_LOOP, playbackIsLoop, STRING_UNDEFINED, BOOL, true) \ X(PLAYBACK_IS_CLAMP, playbackIsClamp, STRING_UNDEFINED, BOOL, true) \ + X(PLAYBACK_TICK_RATE, playbackTickRate, STRING_UNDEFINED, INT, 30) \ \ X(CHANGE_IS_CROP_X, changeIsCropX, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_CROP_Y, changeIsCropY, STRING_UNDEFINED, BOOL, false) \ diff --git a/src/state.cpp b/src/state.cpp index 8668bcc..0fd5354 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -18,8 +18,6 @@ using namespace anm2ed::types; namespace anm2ed { - constexpr auto TICK_RATE = 30; - constexpr auto TICK_INTERVAL = (1000 / TICK_RATE); constexpr auto UPDATE_RATE = 120; constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE); @@ -33,7 +31,7 @@ namespace anm2ed manager.chords_set(settings); } - void State::tick(Settings& settings) { dockspace.tick(manager, settings); } + void State::tick(Settings& settings, float deltaSeconds) { dockspace.tick(manager, settings, deltaSeconds); } void State::update(SDL_Window*& window, Settings& settings) { @@ -160,16 +158,8 @@ namespace anm2ed auto currentTick = SDL_GetTicks(); auto currentUpdate = SDL_GetTicks(); auto isRecording = manager.isRecording; - auto tickIntervalMs = (double)TICK_INTERVAL; - - if (isRecording) - { - if (auto document = manager.get()) - { - auto fps = std::max(document->anm2.info.fps, 1); - tickIntervalMs = std::max(1.0, 1000.0 / (double)fps); - } - } + auto tickRate = std::max(settings.playbackTickRate, 1); + auto tickIntervalMs = 1000.0 / (double)tickRate; if (isRecording != wasRecording) { @@ -194,7 +184,7 @@ namespace anm2ed if (tickAccumulatorMs >= tickIntervalMs) { - tick(settings); + tick(settings, (float)(tickIntervalMs / 1000.0)); tickAccumulatorMs -= tickIntervalMs; } diff --git a/src/state.hpp b/src/state.hpp index 200791e..808e209 100644 --- a/src/state.hpp +++ b/src/state.hpp @@ -8,7 +8,7 @@ namespace anm2ed { class State { - void tick(Settings&); + void tick(Settings&, float); void update(SDL_Window*&, Settings&); void render(SDL_Window*&, Settings&);