Skip to content

Conversation

@imsys
Copy link
Contributor

@imsys imsys commented Sep 18, 2025

clock() was previously replaced by frame time callback (1c8eb84 #2589)
It introduced a few bugs that I tried to fix (#2829)
But there is a benchmark cartridge ( git:demos/benchmark.lua | tic80.com/play ) that calls time() to measure itra-frame timing.
It was broken using the current libretro core, it's runTime was stuck in zero.

Reverting to the use of clock() fixes this issue.

Bellow is Gemini explanation:


The situation involves two different timing requirements:

  • Inter-Frame Timing (Game Speed): This is about ensuring the game logic advances correctly from one frame to the next. If 1/60th of a second has passed in the real world, the game should also advance its state by 1/60th of a second. The original fix (state->frameTime += usec;) solved this by correctly accumulating the time between frames.

  • Intra-Frame Timing (Benchmark Measurement): This is what the benchmark cartridge needs. It uses TIC-80's time() function to measure how long an operation takes within a single frame. It records the start time, does a lot of drawing, records the end time, and calculates the difference.

The original fix failed the benchmark test because the state->frameTime value is only updated once per frame. For the entire duration of the MAINTIC() function, the value returned by time() would be constant. Therefore, time() - stime would always be zero.

--
The clock() solution is superior because it provides a high-resolution, continuously updating timer directly from the system's C library.

  • clock(): This standard function returns the amount of processor time used by the program. Crucially, this value increases during the execution of a single frame.

  • CLOCKS_PER_SEC: This constant provides the frequency of the clock() timer.

When the benchmark calls time() (clock() / CLOCKS_PER_SEC), it gets a precise timestamp. After it finishes its work and calls time() again, the value from clock() will have increased, yielding a correct, non-zero runningTime. This solves the intra-frame timing problem.

Simultaneously, because clock() is a consistently increasing counter, the TIC-80 engine can use it to perfectly measure the duration between frames, solving the inter-frame timing problem as well.

clock() was previously replaced by frame time callback (1c8eb84 nesbox#2589)
It introduced a few bugs that I tried to fix (nesbox#2829)
But there is a [benchmark cartridge](https://tic80.com/play?cart=153)
that calls time() to mesure itra-frame timing.

This is how Gemini explains it:

The situation involves two different timing requirements:

Inter-Frame Timing (Game Speed): This is about ensuring the game
logic advances correctly from one frame to the next. If 1/60th of a
second has passed in the real world, the game should also advance its
state by 1/60th of a second. The original fix (`state->frameTime +=
usec;`) solved this by correctly accumulating the time between frames.

Intra-Frame Timing (Benchmark Measurement): This is what the
benchmark cartridge needs. It uses TIC-80's `time()` function to measure
how long an operation takes within a single frame. It records the start
time, does a lot of drawing, records the end time, and calculates the
difference.

The original fix failed the benchmark test because the
`state->frameTime` value is only updated once per frame. For the entire
duration of the `MAINTIC()` function, the value returned by `time()`
would be constant. Therefore, `time() - stime` would always be zero.

--
The clock() solution is superior because it provides a high-resolution,
continuously updating timer directly from the system's C library.

clock(): This standard function returns the amount of processor time
used by the program. Crucially, this value increases during the
execution of a single frame.

CLOCKS_PER_SEC: This constant provides the frequency of the clock()
timer.

When the benchmark calls time() (`clock() / CLOCKS_PER_SEC`), it gets a
precise timestamp. After it finishes its work and calls time() again,
the value from clock() will have increased, yielding a correct, non-zero
`runningTime`. This solves the intra-frame timing problem.

Simultaneously, because clock() is a consistently increasing counter,
the TIC-80 engine can use it to perfectly measure the duration between
frames, solving the inter-frame timing problem as well.
@imsys
Copy link
Contributor Author

imsys commented Sep 18, 2025

@RobLoach, you might want to look at this. As this is reverting one of your commits.

-DCMAKE_POLICY_VERSION_MINIMUM=3.5
@RobLoach
Copy link
Contributor

RobLoach commented Sep 18, 2025

Frame time callback is usually better since the front-end tries to provide the best time it can. Clock() may not be available in some systems. Also allows the frontend to manipulate the frames as desired.

The front-end may even use clock() to get that frame time value.

You can find some doc's around it over at https://github.com/libretro/RetroArch/blob/master/libretro-common/include/libretro.h

usec is the amount of time that passed since the previous frame, in microseconds. Given that, state->frameTime += usec isn't correct, since that would add up the amount of time for each frame.

Haven't looked at this in a while, where is runTime used? Which frontend are you using?

@imsys
Copy link
Contributor Author

imsys commented Sep 18, 2025

@imsys imsys marked this pull request as draft September 18, 2025 14:53
@imsys
Copy link
Contributor Author

imsys commented Sep 18, 2025

From the tick documentation:
time() -> ticks
"This function returns the number of milliseconds elapsed since the cartridge began execution.
Useful for keeping track of time, animating items and triggering events."

So if time() is used multiple times in a frame, we can't use usec.

I see the advantages of callback would allow to speedup/slowdown, I will have to spend more time to understand how to implement it correctly.

@imsys
Copy link
Contributor Author

imsys commented Sep 19, 2025

// tic80_libretro.c

static u64 tic80_libretro_counter()
{
    if (state == NULL) {
        return 0;
    }
    return (u64)state->frameTime;
}


// Set up the frame time callback.
struct retro_frame_time_callback frame_time = {
    .callback = tic80_libretro_frame_time,
    .reference = TIC80_FREQUENCY / TIC80_FRAMERATE,
};

environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, &frame_time)


tic80_tick(game, state->input, tic80_libretro_counter, tic80_libretro_frequency);
// core.c

// time() // can be called multiple times in the same frame

double tic_api_time(tic_mem* memory)
{
    tic_core* core = (tic_core*)memory;
    return (double)(core->data->counter(core->data->data) - core->data->start) * 1000.0 / core->data->freq(core->data->data);
}


// tic.c

TIC80_API void tic80_tick(tic80* tic, tic80_input input, CounterCallback counter, FreqCallback freq)
{
    tic_mem* mem = (tic_mem*)tic;

    mem->ram->input = input;

    tic_tick_data tickData = (tic_tick_data)
    {
        .error = onError,
        .trace = onTrace,
        .exit = onExit,
        .data = tic,
        .start = 0,
        .counter = counter,
        .freq = freq
    };
 // ...

@imsys
Copy link
Contributor Author

imsys commented Sep 19, 2025

Which frontend are you using?

I'm using the regular Retroarch, testing both on a desktop Linux and Android.

I found an option we can use:

/**
 * Returns an interface that the core can use for profiling code
 * and to access performance-related information.
 *
 * This callback supports performance counters, a high-resolution timer,
 * and listing available CPU features (mostly SIMD instructions).
 *
 * @param[out] data <tt>struct retro_perf_callback *</tt>.
 * Pointer to the callback interface.
 * Behavior is undefined if \c NULL.
 * @returns \c true if the environment call is available.
 * @see retro_perf_callback
 */
#define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28


struct retro_perf_callback
{
   /** @copydoc retro_perf_get_time_usec_t */
   retro_perf_get_time_usec_t    get_time_usec;


   /** @copydoc retro_perf_get_counter_t */
   retro_perf_get_counter_t      get_perf_counter;
 // ....
};

/**
 * @returns The current system time in microseconds.
 * @note Accuracy may vary by platform.
 * The frontend should use the most accurate timer possible.
 * @see RETRO_ENVIRONMENT_GET_PERF_INTERFACE
 */
typedef retro_time_t (RETRO_CALLCONV *retro_perf_get_time_usec_t)(void);

/**
 * @returns The number of ticks since some unspecified epoch.
 * The exact meaning of a "tick" depends on the platform,
 * but it usually refers to nanoseconds or CPU cycles.
 * @see RETRO_ENVIRONMENT_GET_PERF_INTERFACE
 */
typedef retro_perf_tick_t (RETRO_CALLCONV *retro_perf_get_counter_t)(void);

I tested both retro_time_t and retro_perf_tick_t, they both worked for games including speeding up and slowing down, but only retro_time_t worked for the benchmark.

@imsys
Copy link
Contributor Author

imsys commented Sep 19, 2025

It's not final yet, but I think I found a path.

This is best tested on the edge case games listed here: #2820 (comment), those games are quite sensitive to the changes in the counter.

@imsys
Copy link
Contributor Author

imsys commented Sep 19, 2025

api_time_debug.zip
I made a debug cart to make things a bit easier.

@imsys
Copy link
Contributor Author

imsys commented Sep 20, 2025

api_time_debug-250919-220618 api_time_debug-250919-213756 api_time_debug-250919-215802 api_time_debug-250919-213158

From my tests:

Method Frontend time manipulation Precision
frametime callback() ✅It just works! Same as framerate, may fail when the cart require to use time() diff in the same frame.
get_time_usec Maybe difficult to implement, doesn’t seem to be intended usecase Very high precision (nanoseconds)
get_perf_counter difficult Counter has an unknown frequency
clock() difficult Bad precision, difficult to get framerate right, and time diff shows 10x higher than expected, it doesn’t seem reliable.

Tomorrow I may do a performance test just out of curiosity, but I expect the frametime callback would just be superior.

I don't think there are any tic80 game that uses time diffs in the same frame (as I have tested most tic games....) maybe just that benchmark cart that was implemented in a bad way in my opinion. So I don't think there is a need to worry or spend time on this.

Maybe in the future there could be an option of hardware accuracy/precision, but it seems kind of overkill for now.

That being said, the timing is working perfect as it currently is.

@imsys imsys closed this Sep 20, 2025
@RobLoach
Copy link
Contributor

Wow that seems so much better. Nice find. Definitely is tricky to get it all right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants