Refactor, Vsync, FFmpeg fixes

This commit is contained in:
2025-08-15 12:18:57 -04:00
parent e8094a19c4
commit a665023626
15 changed files with 149 additions and 78 deletions

View File

@@ -23,10 +23,9 @@
#include <ranges>
#include <string>
#include <unordered_set>
#include <variant>
#include <vector>
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
@@ -49,10 +48,9 @@ using namespace glm;
#define FLOAT_TO_U8(x) (static_cast<u8>((x) * 255.0f))
#define U8_TO_FLOAT(x) ((x) / 255.0f)
#define PERCENT_TO_UNIT(x) (x / 100.0f)
#define TICK_DELAY 33.3f
#define TICK_CATCH_UP_MAX (33.3f * 5)
#define SECOND 1000.0f
#define TICK_RATE (SECOND / TICK_DELAY)
#define TICK_DELAY (SECOND / 30.0)
#define UPDATE_DELAY (SECOND / 120.0)
#define ID_NONE -1
#define INDEX_NONE -1
#define TIME_NONE -1.0f
@@ -125,6 +123,11 @@ static inline bool string_to_bool(const std::string& string)
return lower == "true";
}
static inline std::string string_quote(const std::string& string)
{
return "\"" + string + "\"";
}
static inline std::string path_canonical_resolve
(
const std::string& inputPath,

View File

@@ -1,7 +1,6 @@
#pragma once
#include "anm2.h"
#include <variant>
enum ClipboardItemType
{
@@ -34,4 +33,3 @@ void clipboard_copy(Clipboard* self);
void clipboard_cut(Clipboard* self);
void clipboard_paste(Clipboard* self);
void clipboard_init(Clipboard* self, Anm2* anm2);

View File

@@ -30,44 +30,43 @@ void dialog_init(Dialog* self, SDL_Window* window)
void dialog_anm2_open(Dialog* self)
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, 1, nullptr, false);
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, std::size(DIALOG_FILE_FILTER_ANM2), nullptr, false);
self->type = DIALOG_ANM2_OPEN;
}
void dialog_anm2_save(Dialog* self)
{
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, 1, nullptr);
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_ANM2, std::size(DIALOG_FILE_FILTER_ANM2), nullptr);
self->type = DIALOG_ANM2_SAVE;
}
void dialog_spritesheet_add(Dialog* self)
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, 1, nullptr, false);
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
self->type = DIALOG_SPRITESHEET_ADD;
}
void dialog_spritesheet_replace(Dialog* self, s32 id)
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, 1, nullptr, false);
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_PNG, std::size(DIALOG_FILE_FILTER_PNG), nullptr, false);
self->replaceID = id;
self->type = DIALOG_SPRITESHEET_REPLACE;
}
void dialog_render_path_set(Dialog* self)
void dialog_render_path_set(Dialog* self, RenderType type)
{
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_RENDER, 2, nullptr);
self->type = DIALOG_RENDER_PATH_SET;
}
SDL_DialogFileFilter filter = DIALOG_RENDER_FILE_FILTERS[type];
void dialog_render_directory_set(Dialog* self)
{
SDL_ShowOpenFolderDialog(_dialog_callback, self, self->window, nullptr, false);
self->type = DIALOG_RENDER_DIRECTORY_SET;
if (type == RENDER_PNG)
SDL_ShowOpenFolderDialog(_dialog_callback, self, self->window, nullptr, false);
else
SDL_ShowSaveFileDialog(_dialog_callback, self, self->window, &filter, 1, nullptr);
self->type = DIALOG_RENDER_PATH_SET;
}
void dialog_ffmpeg_path_set(Dialog* self)
{
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_FFMPEG, 1, nullptr, false);
SDL_ShowOpenFileDialog(_dialog_callback, self, self->window, DIALOG_FILE_FILTER_FFMPEG, std::size(DIALOG_FILE_FILTER_FFMPEG), nullptr, false);
self->type = DIALOG_FFMPEG_PATH_SET;
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include "render.h"
#include "window.h"
const SDL_DialogFileFilter DIALOG_FILE_FILTER_ANM2[] =
@@ -12,15 +13,17 @@ const SDL_DialogFileFilter DIALOG_FILE_FILTER_PNG[] =
{"PNG image", "png"}
};
const SDL_DialogFileFilter DIALOG_FILE_FILTER_RENDER[] =
const SDL_DialogFileFilter DIALOG_RENDER_FILE_FILTERS[] =
{
{"PNG image", "png"},
{"GIF image", "gif"},
{"WebM video", "webm"}
{"WebM video", "webm"},
{"MP4 video", "mp4"}
};
const SDL_DialogFileFilter DIALOG_FILE_FILTER_FFMPEG[] =
{
{"Executable", ""}
{"Executable", "exe"}
};
enum DialogType
@@ -31,7 +34,6 @@ enum DialogType
DIALOG_SPRITESHEET_ADD,
DIALOG_SPRITESHEET_REPLACE,
DIALOG_RENDER_PATH_SET,
DIALOG_RENDER_DIRECTORY_SET,
DIALOG_FFMPEG_PATH_SET
};
@@ -50,7 +52,7 @@ void dialog_anm2_open(Dialog* self);
void dialog_spritesheet_add(Dialog* self);
void dialog_spritesheet_replace(Dialog* self, s32 id);
void dialog_anm2_save(Dialog* self);
void dialog_render_path_set(Dialog* self);
void dialog_render_path_set(Dialog* self, RenderType type);
void dialog_render_directory_set(Dialog* self);
void dialog_ffmpeg_path_set(Dialog* self);
void dialog_reset(Dialog* self);

View File

@@ -23,10 +23,17 @@ ffmpeg_render
case RENDER_WEBM:
command = std::format(FFMPEG_WEBM_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
case RENDER_MP4:
command = std::format(FFMPEG_MP4_FORMAT, ffmpegPath, size.x, size.y, fps, outputPath);
break;
default:
return false;
break;
}
#if _WIN32
command = string_quote(command)
#endif
FILE* fp = POPEN(command.c_str(), PWRITE_MODE);
if (!fp)

View File

@@ -6,19 +6,25 @@
#define FFMPEG_POPEN_ERROR "popen() (for FFmpeg) failed!\n{}"
static constexpr const char* FFMPEG_GIF_FORMAT =
"\"{0}\" -y "
"{0} -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-lavfi \"split[s0][s1];"
"[s0]palettegen=stats_mode=full[p];"
"[s1][p]paletteuse=dither=floyd_steinberg\" "
"-loop 0 \"{4}\"";
"-loop 0 {4}";
static constexpr const char* FFMPEG_WEBM_FORMAT =
"\"{0}\" -y "
"{0} -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 "
"-auto-alt-ref 0 -an \"{4}\"";
"-auto-alt-ref 0 -an {4}";
static constexpr const char* FFMPEG_MP4_FORMAT =
"{0} -y "
"-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 "
"-vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" "
"-c:v libx265 -crf 20 -preset slow "
"-tag:v hvc1 -movflags +faststart -an {4}";
bool
ffmpeg_render

View File

@@ -354,10 +354,11 @@ static bool NAME(const ImguiItem& self, Imgui* imgui, bool& boolValue)
ImguiItem checkboxItem = self.copy \
({.label = std::format(IMGUI_INVISIBLE_FORMAT, self.label), .isMnemonicDisabled = true}); \
checkboxItem.isDisabled = false; \
_imgui_checkbox(checkboxItem, imgui, boolValue); \
bool isCheckboxActivated = _imgui_checkbox(checkboxItem, imgui, boolValue); \
ImGui::SameLine(); \
bool isActivated = ([&] { return FUNCTION; })(); \
if (isActivated) boolValue = !boolValue; \
if (isCheckboxActivated) isActivated = true; \
return isActivated; \
}
@@ -367,9 +368,10 @@ static bool NAME(const ImguiItem& self, Imgui* imgui, VALUE& value, bool& boolVa
ImguiItem checkboxItem = self.copy \
({.label = std::format(IMGUI_INVISIBLE_FORMAT, self.label), .isMnemonicDisabled = true}); \
checkboxItem.isDisabled = false; \
_imgui_checkbox(checkboxItem, imgui, boolValue); \
bool isCheckboxActivated = _imgui_checkbox(checkboxItem, imgui, boolValue); \
ImGui::SameLine(); \
bool isActivated = ([&](VALUE& value) { return FUNCTION; })(value); \
if (isCheckboxActivated) isActivated = true; \
return isActivated; \
}
@@ -1413,49 +1415,45 @@ static void _imgui_taskbar(Imgui* self)
if (imgui_begin_popup_modal(IMGUI_RENDER_ANIMATION.popup, self, IMGUI_RENDER_ANIMATION.popupSize))
{
static DialogType& dialogType = self->dialog->type;
static bool& dialogIsSelected = self->dialog->isSelected;
static s32& type = self->settings->renderType;
static std::string& dialogPath = self->dialog->path;
static std::string& ffmpegPath = self->settings->ffmpegPath;
static std::string& format = self->settings->renderFormat;
static std::string& path = self->settings->renderPath;
_imgui_begin_child(IMGUI_RENDER_ANIMATION_CHILD, self);
if (_imgui_atlas_button(IMGUI_RENDER_ANIMATION_LOCATION_BROWSE, self))
{
switch (self->settings->renderType)
{
case RENDER_PNG: dialog_render_directory_set(self->dialog); break;
default:
dialog_render_path_set(self->dialog);
break;
}
}
dialog_render_path_set(self->dialog, (RenderType)type);
if
(
self->dialog->isSelected &&
(self->dialog->type == DIALOG_RENDER_PATH_SET || self->dialog->type == DIALOG_RENDER_DIRECTORY_SET)
)
if (dialogIsSelected && (dialogType == DIALOG_RENDER_PATH_SET))
{
self->settings->renderPath = self->dialog->path;
path = path_extension_change(dialogPath, RENDER_EXTENSIONS[type]);
dialog_reset(self->dialog);
}
_imgui_input_text(IMGUI_RENDER_ANIMATION_LOCATION, self, self->settings->renderPath);
_imgui_input_text(IMGUI_RENDER_ANIMATION_LOCATION, self, path);
if (_imgui_atlas_button(IMGUI_RENDER_ANIMATION_FFMPEG_BROWSE, self))
dialog_ffmpeg_path_set(self->dialog);
if (self->dialog->isSelected && self->dialog->type == DIALOG_FFMPEG_PATH_SET)
if (dialogIsSelected && dialogType == DIALOG_FFMPEG_PATH_SET)
{
self->settings->ffmpegPath = self->dialog->path;
ffmpegPath = self->dialog->path;
dialog_reset(self->dialog);
}
_imgui_input_text(IMGUI_RENDER_ANIMATION_FFMPEG_PATH, self, self->settings->ffmpegPath);
_imgui_input_text(IMGUI_RENDER_ANIMATION_FORMAT, self, self->settings->renderFormat);
_imgui_combo(IMGUI_RENDER_ANIMATION_OUTPUT, self, &self->settings->renderType);
_imgui_input_text(IMGUI_RENDER_ANIMATION_FFMPEG_PATH, self, ffmpegPath);
_imgui_input_text(IMGUI_RENDER_ANIMATION_FORMAT, self, format);
_imgui_combo(IMGUI_RENDER_ANIMATION_OUTPUT, self, &type);
if (_imgui_button(IMGUI_RENDER_ANIMATION_CONFIRM, self))
{
bool isRenderStart = true;
if (!std::filesystem::exists(self->settings->ffmpegPath))
if (!std::filesystem::exists(ffmpegPath))
{
imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_FFMPEG_PATH_ERROR);
isRenderStart = false;
@@ -1466,7 +1464,7 @@ static void _imgui_taskbar(Imgui* self)
switch (self->settings->renderType)
{
case RENDER_PNG:
if (!std::filesystem::is_directory(self->settings->renderPath))
if (!std::filesystem::is_directory(path))
{
imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_DIRECTORY_ERROR);
isRenderStart = false;
@@ -1474,7 +1472,8 @@ static void _imgui_taskbar(Imgui* self)
break;
case RENDER_GIF:
case RENDER_WEBM:
if (!path_is_valid(self->settings->renderPath))
case RENDER_MP4:
if (!path_is_valid(path))
{
imgui_log_push(self, IMGUI_LOG_RENDER_ANIMATION_PATH_ERROR);
isRenderStart = false;
@@ -1502,6 +1501,9 @@ static void _imgui_taskbar(Imgui* self)
if (imgui_begin_popup_modal(IMGUI_RENDER_ANIMATION_CONFIRM.popup, self, IMGUI_RENDER_ANIMATION_CONFIRM.popupSize))
{
static s32& type = self->settings->renderType;
static std::string& format = self->settings->renderFormat;
auto rendering_end = [&]()
{
preview_render_end(self->preview);
@@ -1537,7 +1539,7 @@ static void _imgui_taskbar(Imgui* self)
if (self->preview->isRenderFinished)
{
switch (self->settings->renderType)
switch (type)
{
case RENDER_PNG:
{
@@ -1546,10 +1548,9 @@ static void _imgui_taskbar(Imgui* self)
for (auto [i, frame] : std::views::enumerate(frames))
{
std::string framePath = std::vformat(self->settings->renderFormat, std::make_format_args(i));
framePath = path_extension_change(framePath, RENDER_EXTENSIONS[self->settings->renderType]);
if (!frame.isInvalid)
texture_from_gl_write(&frame, framePath);
std::string framePath = std::vformat(format, std::make_format_args(i));
framePath = path_extension_change(framePath, RENDER_EXTENSIONS[type]);
if (!frame.isInvalid) texture_from_gl_write(&frame, framePath);
}
std::filesystem::current_path(workingPath);
@@ -1558,11 +1559,12 @@ static void _imgui_taskbar(Imgui* self)
}
case RENDER_GIF:
case RENDER_WEBM:
case RENDER_MP4:
{
std::string ffmpegPath = std::string(self->settings->ffmpegPath.c_str());
path = path_extension_change(path, RENDER_EXTENSIONS[self->settings->renderType]);
if (ffmpeg_render(ffmpegPath, path, frames, self->preview->canvas.size, self->anm2->fps, (RenderType)self->settings->renderType))
if (ffmpeg_render(ffmpegPath, path, frames, self->preview->canvas.size, self->anm2->fps, (RenderType)type))
imgui_log_push(self, std::format(IMGUI_LOG_RENDER_ANIMATION_SAVE_FORMAT, path));
else
imgui_log_push(self, std::format(IMGUI_LOG_RENDER_ANIMATION_FFMPEG_ERROR, path));
@@ -1587,6 +1589,14 @@ static void _imgui_taskbar(Imgui* self)
imgui_end_popup(self);
}
_imgui_selectable(IMGUI_SETTINGS.copy({}), self);
if (imgui_begin_popup(IMGUI_SETTINGS.popup, self, IMGUI_SETTINGS.popupSize))
{
if (_imgui_checkbox_selectable(IMGUI_VSYNC, self, self->settings->isVsync)) window_vsync_set(self->settings->isVsync);
imgui_end_popup(self);
}
_imgui_end();
}

View File

@@ -975,8 +975,8 @@ IMGUI_ITEM(IMGUI_PLAYBACK,
self.tooltip = "Opens the playback menu, for configuring playback settings.",
self.popup = "## Playback Popup",
self.popupType = IMGUI_POPUP_BY_ITEM,
self.popupSize = {150, 0},
self.isSizeToText = true
self.isSizeToText = true,
self.isSameLine = true
);
IMGUI_ITEM(IMGUI_ALWAYS_LOOP,
@@ -991,6 +991,20 @@ IMGUI_ITEM(IMGUI_CLAMP_PLAYHEAD,
self.isSizeToText = true
);
IMGUI_ITEM(IMGUI_SETTINGS,
self.label = "&Settings",
self.tooltip = "Opens the setting menu, for configuring general program settings.",
self.popup = "## Settings Popup",
self.popupType = IMGUI_POPUP_BY_ITEM,
self.isSizeToText = true
);
IMGUI_ITEM(IMGUI_VSYNC,
self.label = "&Vsync",
self.tooltip = "Toggle vertical sync; synchronizes program framerate with your monitor's refresh rate.",
self.isSizeToText = true
);
IMGUI_ITEM(IMGUI_ANIMATIONS,
self.label = "Animations",
self.flags = ImGuiWindowFlags_NoScrollbar |

View File

@@ -44,7 +44,7 @@ void preview_tick(Preview* self)
self->renderFrames.push_back(frameTexture);
}
time += (f32)self->anm2->fps / TICK_RATE;
time += (f32)self->anm2->fps / TICK_DELAY;
if (time >= (f32)animation->frameNum - 1)
{
@@ -72,10 +72,7 @@ void preview_tick(Preview* self)
time = std::clamp(time, 0.0f, std::max(0.0f, (f32)animation->frameNum - 1));
else
time = std::max(time, 0.0f);
}
}
void preview_draw(Preview* self)

View File

@@ -7,19 +7,23 @@ enum RenderType
RENDER_PNG,
RENDER_GIF,
RENDER_WEBM,
RENDER_COUNT
RENDER_MP4
};
constexpr inline s32 RENDER_COUNT = RENDER_MP4 + 1;
const inline std::string RENDER_TYPE_STRINGS[] =
{
"PNG Images",
"GIF image",
"WebM video",
"MP4 video"
};
const inline std::string RENDER_EXTENSIONS[RENDER_COUNT] =
{
".png",
".gif",
".webm"
".webm",
".mp4"
};

View File

@@ -32,6 +32,7 @@ struct SettingsEntry
struct Settings
{
ivec2 windowSize = {1080, 720};
bool isVsync = true;
bool playbackIsLoop = true;
bool playbackIsClampPlayhead = true;
bool changeIsCrop = false;
@@ -106,6 +107,7 @@ struct Settings
const SettingsEntry SETTINGS_ENTRIES[] =
{
{"window", TYPE_IVEC2, offsetof(Settings, windowSize)},
{"isVsync", TYPE_BOOL, offsetof(Settings, isVsync)},
{"playbackIsLoop", TYPE_BOOL, offsetof(Settings, playbackIsLoop)},
{"playbackIsClampPlayhead", TYPE_BOOL, offsetof(Settings, playbackIsClampPlayhead)},
{"changeIsCrop", TYPE_BOOL, offsetof(Settings, changeIsCrop)},
@@ -182,6 +184,7 @@ const std::string SETTINGS_DEFAULT = R"(
[Settings]
windowX=1920
windowY=1080
isVsync=true
playbackIsLoop=true
playbackIsClampPlayhead=false
changeIsCrop=false

View File

@@ -79,6 +79,8 @@ void init(State* self)
self->glContext = SDL_GL_CreateContext(self->window);
window_vsync_set(self->settings.isVsync);
if (!self->glContext)
{
log_error(std::format(STATE_GL_CONTEXT_INIT_ERROR, SDL_GetError()));
@@ -96,8 +98,6 @@ void init(State* self)
glDisable(GL_DEPTH_TEST);
glDisable(GL_LINE_SMOOTH);
SDL_GL_SetSwapInterval(1);
resources_init(&self->resources);
dialog_init(&self->dialog, self->window);
clipboard_init(&self->clipboard, &self->anm2);
@@ -135,6 +135,7 @@ void init(State* self)
void loop(State* self)
{
self->tick = SDL_GetTicks();
self->update = self->tick;
while (self->tick > self->lastTick + TICK_DELAY)
{
@@ -148,10 +149,26 @@ void loop(State* self)
self->lastTick = self->tick;
}
_update(self);
_draw(self);
if (self->settings.isVsync)
{
_update(self);
_draw(self);
}
else
{
while (self->update > self->lastUpdate + UPDATE_DELAY)
{
self->update = SDL_GetTicks();
SDL_Delay(STATE_DELAY_MIN);
if (self->update - self->lastUpdate < UPDATE_DELAY)
SDL_Delay(UPDATE_DELAY - (self->update - self->lastUpdate));
_update(self);
_draw(self);
self->lastUpdate = self->update;
}
}
}
void quit(State* self)

View File

@@ -43,6 +43,8 @@ struct State
std::string lastAction{};
u64 lastTick{};
u64 tick{};
u64 update{};
u64 lastUpdate{};
bool isRunning = true;
};

View File

@@ -7,3 +7,11 @@ void window_title_from_path_set(SDL_Window* self, const std::string& path)
else
SDL_SetWindowTitle(self, WINDOW_TITLE);
}
void window_vsync_set(bool isVsync)
{
if (isVsync)
SDL_GL_SetSwapInterval(1);
else
SDL_GL_SetSwapInterval(0);
}

View File

@@ -8,3 +8,4 @@
void window_title_from_path_set(SDL_Window* self, const std::string& path);
bool window_color_from_position_get(SDL_Window* self, vec2 position, vec4* color);
void window_vsync_set(bool isVsync);