Skip to content

Commit f9eace3

Browse files
committed
feat(examples): run tween on SDL OpenGL host
1 parent 3a2446c commit f9eace3

6 files changed

Lines changed: 265 additions & 3 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ The development and release presets build the native examples. Run the compiler-
5050

5151
The example preserves the fifteen easing tracks from Flight's TypeScript tween example and renders one deterministic frame in a terminal. Its portable calculation is TypeScript transpiled by the pinned `flight-compiler`; a small handwritten C++ host owns terminal output. See [`examples/README.md`](examples/README.md) for the source, generated output, regeneration command, and the current boundary around browser-backed examples.
5252

53+
The same generated curves also have an interactive SDL/OpenGL ES host. Enable the optional SDL package and disable the
54+
unused Vulkan adapter, then run the native window:
55+
56+
```sh
57+
cmake --preset development \
58+
-DFLIGHT_CPP_BUILD_HOST_SDL=ON \
59+
-DFLIGHT_CPP_BUILD_HOST_SDL_VULKAN=OFF
60+
cmake --build --preset development
61+
./out/cmake/development/examples/flight_cpp_tween_sdl_gl_example
62+
```
63+
5364
Consumers can build it in-tree with `add_subdirectory`, or install it and use:
5465

5566
```cmake

docs/host-sdl.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ cmake --build --preset development
3131
ctest --preset development
3232
```
3333

34+
With examples enabled by the preset, the native tween example exercises the GL path end to end:
35+
36+
```sh
37+
./out/cmake/development/examples/flight_cpp_tween_sdl_gl_example
38+
```
39+
40+
It animates the fifteen easing curves emitted from `examples/tween/source/tween.ts`. Rendering uses only the OpenGL ES
41+
context and procedure loader exposed by `Flight::HostSdlGl`; it does not introduce an SDL renderer abstraction.
42+
3443
Set `-DFLIGHT_CPP_BUILD_HOST_SDL_VULKAN=OFF` for an SDL and GL/WGPU build without Vulkan development files. Native
3544
dependency discovery and target selection belong to CMake, so an npm wrapper would only obscure the options and is
3645
not provided. Bazel continues to cover the dependency-free runtime; the optional SDL package currently has a CMake

examples/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ add_executable(flight_cpp_tween_example tween/tween_example.cpp)
22
target_link_libraries(flight_cpp_tween_example PRIVATE Flight::Cpp)
33
flight_cpp_set_project_warnings(flight_cpp_tween_example)
44

5+
if(TARGET Flight::HostSdlGl)
6+
add_executable(flight_cpp_tween_sdl_gl_example tween/tween_sdl_gl_example.cpp)
7+
target_link_libraries(flight_cpp_tween_sdl_gl_example PRIVATE Flight::HostSdlGl)
8+
flight_cpp_set_project_warnings(flight_cpp_tween_sdl_gl_example)
9+
endif()
10+
511
if(BUILD_TESTING)
612
add_test(NAME flight_cpp.example.tween COMMAND flight_cpp_tween_example)
713
endif()

examples/README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ cmake --build --preset development
1414
./out/cmake/development/examples/flight_cpp_tween_example
1515
```
1616

17+
The SDL/OpenGL ES entry point renders all fifteen generated easing tracks in a native window. It uses the handwritten
18+
SDL host for the window, event loop, GL context, procedure loading, and presentation; the curve calculations still
19+
come from the transpiled TypeScript above. Configure the optional host without requiring Vulkan, then run it:
20+
21+
```sh
22+
cmake --preset development \
23+
-DFLIGHT_CPP_BUILD_HOST_SDL=ON \
24+
-DFLIGHT_CPP_BUILD_HOST_SDL_VULKAN=OFF
25+
cmake --build --preset development
26+
./out/cmake/development/examples/flight_cpp_tween_sdl_gl_example
27+
```
28+
29+
Close the window or press Escape to exit. The executable also accepts `--smoke`, which creates a hidden GL window,
30+
renders three frames, and exits; this is useful with an offscreen SDL video driver in automated environments.
31+
1732
To regenerate `tween/generated/tween.hpp` with the compiler revision pinned by this repository:
1833

1934
```sh
@@ -26,4 +41,9 @@ npm --prefix .dependencies/flight-compiler run compile -- \
2641
--package @flighthq/examples-tween
2742
```
2843

29-
The upstream browser entry point itself cannot currently be transpiled. It contains executable module-level statements and browser host calls; the pinned compiler refuses those statements rather than emitting invalid C++. The portable source also spells the elastic curve's phase shift as the equivalent `period / 4`, because the compiler currently emits `Math.asin` with an invalid C++ qualifier. Keeping the curve calculation in a separate source module makes both supported compiler boundaries visible and gives later compiler revisions a straightforward place to remove the adaptations.
44+
The upstream browser entry point itself cannot currently be transpiled. It contains executable module-level statements
45+
and browser host calls; the pinned compiler refuses those statements rather than emitting invalid C++. The portable
46+
source also spells the elastic curve's phase shift as the equivalent `period / 4`, because the compiler currently
47+
emits `Math.asin` with an invalid C++ qualifier. Keeping the curve calculation in a separate source module makes both
48+
supported compiler boundaries visible and gives later compiler revisions a straightforward place to remove the
49+
adaptations.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#include "generated/tween.hpp"
2+
3+
#include <flight/host_sdl/gl.hpp>
4+
#include <flight/host_sdl/host.hpp>
5+
#include <flight/host_sdl/window.hpp>
6+
7+
#include <SDL3/SDL_keycode.h>
8+
#include <SDL3/SDL_opengles2.h>
9+
10+
#include <algorithm>
11+
#include <array>
12+
#include <chrono>
13+
#include <cmath>
14+
#include <cstddef>
15+
#include <exception>
16+
#include <iostream>
17+
#include <stdexcept>
18+
#include <string_view>
19+
#include <thread>
20+
#include <type_traits>
21+
22+
namespace {
23+
24+
struct Color {
25+
GLfloat red;
26+
GLfloat green;
27+
GLfloat blue;
28+
};
29+
30+
template <typename Function>
31+
Function load_gl_function(const flight::host_sdl::GlContext& context, std::string_view name) {
32+
static_assert(std::is_pointer_v<Function>);
33+
return reinterpret_cast<Function>(context.function_address(name));
34+
}
35+
36+
class GlFunctions final {
37+
public:
38+
explicit GlFunctions(const flight::host_sdl::GlContext& context)
39+
: clear(load_gl_function<PFNGLCLEARPROC>(context, "glClear")),
40+
clear_color(load_gl_function<PFNGLCLEARCOLORPROC>(context, "glClearColor")),
41+
disable(load_gl_function<PFNGLDISABLEPROC>(context, "glDisable")),
42+
enable(load_gl_function<PFNGLENABLEPROC>(context, "glEnable")),
43+
get_error(load_gl_function<PFNGLGETERRORPROC>(context, "glGetError")),
44+
scissor(load_gl_function<PFNGLSCISSORPROC>(context, "glScissor")),
45+
viewport(load_gl_function<PFNGLVIEWPORTPROC>(context, "glViewport")) {}
46+
47+
PFNGLCLEARPROC clear;
48+
PFNGLCLEARCOLORPROC clear_color;
49+
PFNGLDISABLEPROC disable;
50+
PFNGLENABLEPROC enable;
51+
PFNGLGETERRORPROC get_error;
52+
PFNGLSCISSORPROC scissor;
53+
PFNGLVIEWPORTPROC viewport;
54+
};
55+
56+
void fill_rectangle(
57+
const GlFunctions& gl,
58+
flight::host_sdl::WindowSize viewport,
59+
int x,
60+
int y,
61+
int width,
62+
int height,
63+
Color color) {
64+
const int left = std::clamp(x, 0, viewport.width);
65+
const int bottom = std::clamp(y, 0, viewport.height);
66+
const int right = std::clamp(x + width, 0, viewport.width);
67+
const int top = std::clamp(y + height, 0, viewport.height);
68+
if (right <= left || top <= bottom) return;
69+
gl.scissor(left, bottom, right - left, top - bottom);
70+
gl.clear_color(color.red, color.green, color.blue, 1.0F);
71+
gl.clear(GL_COLOR_BUFFER_BIT);
72+
}
73+
74+
void fill_circle(
75+
const GlFunctions& gl,
76+
flight::host_sdl::WindowSize viewport,
77+
int center_x,
78+
int center_y,
79+
int radius,
80+
Color color) {
81+
const int radius_squared = radius * radius;
82+
for (int offset_y = -radius; offset_y <= radius; ++offset_y) {
83+
const auto extent = static_cast<int>(
84+
std::sqrt(static_cast<double>(radius_squared - (offset_y * offset_y))));
85+
fill_rectangle(
86+
gl,
87+
viewport,
88+
center_x - extent,
89+
center_y + offset_y,
90+
(extent * 2) + 1,
91+
1,
92+
color);
93+
}
94+
}
95+
96+
void draw_frame(
97+
const GlFunctions& gl,
98+
flight::host_sdl::WindowSize size,
99+
double progress) {
100+
constexpr Color background{0.035F, 0.047F, 0.075F};
101+
constexpr Color track{0.16F, 0.19F, 0.25F};
102+
constexpr std::array<Color, 15> colors{{
103+
{0.96F, 0.38F, 0.38F},
104+
{0.96F, 0.55F, 0.30F},
105+
{0.96F, 0.72F, 0.28F},
106+
{0.78F, 0.80F, 0.28F},
107+
{0.53F, 0.82F, 0.34F},
108+
{0.30F, 0.82F, 0.53F},
109+
{0.25F, 0.80F, 0.72F},
110+
{0.23F, 0.72F, 0.88F},
111+
{0.30F, 0.58F, 0.96F},
112+
{0.45F, 0.48F, 0.96F},
113+
{0.62F, 0.42F, 0.94F},
114+
{0.77F, 0.38F, 0.88F},
115+
{0.90F, 0.36F, 0.72F},
116+
{0.96F, 0.36F, 0.56F},
117+
{0.96F, 0.42F, 0.44F},
118+
}};
119+
120+
gl.viewport(0, 0, size.width, size.height);
121+
gl.disable(GL_SCISSOR_TEST);
122+
gl.clear_color(background.red, background.green, background.blue, 1.0F);
123+
gl.clear(GL_COLOR_BUFFER_BIT);
124+
gl.enable(GL_SCISSOR_TEST);
125+
126+
const auto values = flighthq_examples_tween::sample_tween_curves(progress);
127+
if (values.size() != colors.size()) throw std::runtime_error("unexpected tween curve count");
128+
129+
const int horizontal_margin = std::max(28, size.width / 16);
130+
const int vertical_margin = std::max(20, size.height / 24);
131+
const int available_height = std::max(1, size.height - (vertical_margin * 2));
132+
const int row_height = std::max(1, available_height / static_cast<int>(colors.size()));
133+
const int radius = std::clamp(row_height / 4, 4, 12);
134+
const int track_left = horizontal_margin + radius;
135+
const int track_right = std::max(track_left, size.width - horizontal_margin - radius);
136+
const int track_width = track_right - track_left;
137+
138+
for (std::size_t index = 0; index < colors.size(); ++index) {
139+
const int center_y =
140+
size.height - vertical_margin -
141+
static_cast<int>((static_cast<double>(index) + 0.5) * static_cast<double>(row_height));
142+
fill_rectangle(gl, size, track_left, center_y - 1, track_width, 3, track);
143+
const double value = std::clamp(values[index], 0.0, 1.0);
144+
const int center_x = track_left + static_cast<int>(std::lround(value * static_cast<double>(track_width)));
145+
fill_circle(gl, size, center_x, center_y, radius, colors[index]);
146+
}
147+
gl.disable(GL_SCISSOR_TEST);
148+
if (gl.get_error() != GL_NO_ERROR) throw std::runtime_error("OpenGL rejected the tween frame");
149+
}
150+
151+
int run(bool smoke) {
152+
using namespace std::chrono_literals;
153+
154+
flight::host_sdl::Host host;
155+
flight::host_sdl::WindowOptions options;
156+
options.title = "Flight tween - SDL + OpenGL ES";
157+
options.width = 960;
158+
options.height = 720;
159+
options.graphics_api = flight::host_sdl::GraphicsApi::open_gl;
160+
options.hidden = smoke;
161+
options.open_gl.major_version = 2;
162+
options.open_gl.minor_version = 0;
163+
options.open_gl.profile = flight::host_sdl::OpenGlProfile::es;
164+
165+
flight::host_sdl::Window window(options);
166+
flight::host_sdl::GlContext context(window);
167+
context.make_current(window);
168+
const GlFunctions gl(context);
169+
170+
const std::uint64_t started = flight::host_sdl::Host::ticks_nanoseconds();
171+
std::size_t frames = 0;
172+
bool running = true;
173+
while (running) {
174+
SDL_Event event{};
175+
while (host.poll_event(event)) {
176+
if (event.type == SDL_EVENT_QUIT ||
177+
(event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)) {
178+
running = false;
179+
}
180+
}
181+
182+
const std::uint64_t elapsed = flight::host_sdl::Host::ticks_nanoseconds() - started;
183+
const double seconds = static_cast<double>(elapsed) / 1'000'000'000.0;
184+
const double cycle = std::fmod(seconds / 2.0, 2.0);
185+
const double progress = cycle <= 1.0 ? cycle : 2.0 - cycle;
186+
draw_frame(gl, window.pixel_size(), progress);
187+
context.swap(window);
188+
189+
++frames;
190+
if (smoke && frames >= 3) running = false;
191+
std::this_thread::sleep_for(8ms);
192+
}
193+
return 0;
194+
}
195+
196+
} // namespace
197+
198+
int main(int argc, char** argv) {
199+
if (argc > 2 || (argc == 2 && std::string_view(argv[1]) != "--smoke")) {
200+
std::cerr << "usage: flight_cpp_tween_sdl_gl_example [--smoke]\n";
201+
return 2;
202+
}
203+
try {
204+
return run(argc == 2);
205+
} catch (const std::exception& error) {
206+
std::cerr << "Flight SDL tween failed: " << error.what() << '\n';
207+
return 1;
208+
}
209+
}

scripts/buildHealth.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,14 @@ const benchmarkSources = filesUnder(path.join(cppRoot, 'benchmarks'), isNativeSo
8282
for (const source of benchmarkSources) requireSourceInBothBuilds(source, 'benchmark source');
8383

8484
const exampleSources = filesUnder(path.join(cppRoot, 'examples'), isNativeSource);
85-
for (const source of exampleSources) requireSourceInBothBuilds(source, 'example source');
85+
const hostSdlExampleSources = exampleSources.filter(
86+
(filename) => path.basename(filename) === 'tween_sdl_gl_example.cpp',
87+
);
88+
const portableExampleSources = exampleSources.filter((filename) => !hostSdlExampleSources.includes(filename));
89+
for (const source of portableExampleSources) requireSourceInBothBuilds(source, 'example source');
90+
for (const source of hostSdlExampleSources) {
91+
requireText(cmakeGraph, path.basename(source), `CMake SDL example source ${path.basename(source)}`);
92+
}
8693

8794
if (presets.version !== 2 || presets.cmakeMinimumRequired?.major !== 3 || presets.cmakeMinimumRequired.minor !== 20) {
8895
failures.push('CMakePresets.json must remain usable with the declared CMake 3.20 floor');
@@ -119,7 +126,7 @@ if (failures.length > 0) {
119126
}
120127

121128
process.stdout.write(
122-
`C++ build metadata agrees across CMake and Bazel ${bazelVersion}: ${String(publicHeaders.length)} public headers, ${String(productionSources.length)} runtime source(s), ${String(executableTests.length)} executable test source(s), ${String(benchmarkSources.length)} benchmark source(s), and ${String(exampleSources.length)} example source(s); CMake additionally declares ${String(hostSdlSources.length)} optional SDL host source(s).\n`,
129+
`C++ build metadata agrees across CMake and Bazel ${bazelVersion}: ${String(publicHeaders.length)} public headers, ${String(productionSources.length)} runtime source(s), ${String(executableTests.length)} executable test source(s), ${String(benchmarkSources.length)} benchmark source(s), and ${String(portableExampleSources.length)} portable example source(s); CMake additionally declares ${String(hostSdlSources.length)} optional SDL host source(s) and ${String(hostSdlExampleSources.length)} SDL example source(s).\n`,
123130
);
124131

125132
function filesUnder(directory, include) {

0 commit comments

Comments
 (0)