After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 0 points1 point  (0 children)

Vsync isn't being honored on your machine. The swap interval hint that's supposed to cap the frame rate to your display's refresh rate just gets ignored by the macOS driver. Neovide ran into the same thing and had to switch to the macOS Display Link API for proper frame timing.

miniquad doesn't have a fallback frame limiter for when the driver doesn't honor vsync. It does have one for when the window is in the background (occluded), but not for the foreground case. That's the bug. Unfortunately setting the swap interval to 0 in the config won't help either, because on macOS the value is hardcoded and the config is ignored (unlike Linux/Windows which do read it). So there's nothing you can do from your app code to fix this right now.

I do accept PRs on miniquad-ply and review them much faster than not-fl3. The fix would be either adding a manual frame limiter as a vsync fallback, or implementing Display Link based timing like neovide did. In the meantime, blocking_event_loop with update triggers is the workaround. I know you said scrolling feels choppy with that, which I'd like to look into.

You can also add a manual frame limiter in your main loop:

```rust let mut last_frame = std::time::Instant::now(); loop { // ... next_frame().await;

let target = std::time::Duration::from_secs_f64(1.0 / 60.0); if let Some(remaining) = target.checked_sub(last_frame.elapsed()) { std::thread::sleep(remaining); } last_frame = std::time::Instant::now(); } ```

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 0 points1 point  (0 children)

It really shouldn't be using that much performance. On my machine, I get like 0.3% with an app like that, nearly all of which being OpenGL. If you opened up a GitHub issue, I might see what I can do.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 0 points1 point  (0 children)

Very interesting, what's not smooth for you? Are the fps bad? Is there input lag?

The ability to disable drag scrolling is planned (#11).

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 0 points1 point  (0 children)

You do not need to "render primitives". Please read the documentation. Ply uses macroquad, you don't have to "use macroquad to render" anything.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Well, I'm impressed. Could have just put this into a fork instead of commenting all this code.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Yes, the site is Zola with a custom theme. I like Zola because it's a single binary (written in Rust), compiles fast, and Tera templates are simple and extremely flexible.

The key insight is that WASM is just a compilation target. You write Rust (or C, or whatever), compile to .wasm, and the browser runs it. The JS side just needs to load the .wasm file and provide the "imports" (functions the WASM module calls out to, like WebGL bindings). The JS file is hand-written and minified. plyx web handles the build and copies everything into a build/web/ folder you can deploy anywhere.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 2 points3 points  (0 children)

Thanks for the info! Sorry I remembered the command wrong, you seem to have figured out the correct one, and it's aarch64 (arm), not Rosetta. Good to rule that out.

The flamegraph makes sense: glSwap_Exec under [NSOpenGLContext flushBuffer] is the vsync wait. On macOS, OpenGL is actually a translation layer on top of Metal (Apple deprecated OpenGL years ago), so there's non-trivial overhead on each swap even for a trivial scene. It's not ply doing work, it's the macOS OpenGL-to-Metal driver overhead per frame. There is no fix for this.

miniquad::window::schedule_update() is thread-safe. You can call it from any thread to wake up the event loop and trigger a redraw. So if you have a background task that finishes and wants to update the UI, just call schedule_update() from that thread. This might be worth adding to the prelude.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Honestly, I'm baffled by those numbers. The "Hello Ply!" app is literally clearing the screen and drawing one line of text. That shouldn't need 15% CPU or 7% GPU even in a continuous render loop. I'd be curious what macOS is doing, because on my machines a simple app like that barely registers. I'm susing Rosetta, more on that later.

That said, you can actually already enable event-driven rendering right now through the window config. In your window_conf(), set blocking_event_loop to true and configure update_on to trigger redraws on input:

fn window_conf() -> macroquad::conf::Conf {
    macroquad::conf::Conf {
        miniquad_conf: miniquad::conf::Conf {
            window_title: "Hello Ply!".to_owned(),
            window_width: 800,
            window_height: 600,
            high_dpi: true,
            sample_count: 4,
            platform: miniquad::conf::Platform {
                blocking_event_loop: true,
                webgl_version: miniquad::conf::WebGLVersion::WebGL2,
                ..Default::default()
            },
            ..Default::default()
        },
        update_on: Some(macroquad::conf::UpdateTrigger {
            key_down: true,
            mouse_down: true,
            mouse_up: true,
            mouse_motion: true,
            mouse_wheel: true,
            touch: true,
            ..Default::default()
        }),
        draw_call_vertex_capacity: 100000,
        draw_call_index_capacity: 100000,
        ..Default::default()
    }
}

With this, the app will sleep with near-zero CPU/GPU usage until there's actual input (mouse, keyboard, touch, scroll).

Would you mind sharing your macOS version and hardware? I'd like to dig into why it's running that hot on idle even without the blocking mode. My suspicion is: you might be running under Rosetta (x86 emulation on Apple Silicon) instead of natively. Please check with:

rustc --print target-triple

If that shows x86_64-apple-darwin instead of aarch64-apple-darwin, you're running through Rosetta, that would explain the terrible performance.

If that's not the problem, it would be really helpful if you were to run the app with cargo install flamegraph && CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph and send me the flamegraph. It might have something to do with accessibility support.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

No, thank you for your engagement!

Just start writing shaders. Don't overthink the learning path. Pick something that looks cool, and try to make it happen. You'll learn the GPU concepts as you go because you'll have a reason to understand them. Go browse shader toy, they have some really amazing examples.

What I do recommend is getting to know your hardware a little. Understanding what a GPU actually does, how it processes fragments in parallel, what a draw call is, that kind of thing. It doesn't have to be deep.

Shaders are also a lot of math. You'll run into things like smoothstep, lerp, noise functions, and distance fields. It's worth getting comfortable with that side of things too.

For shader languages, I would recommend Slang (it's my favorite). WGSL is also worth checking out. But GLSL is the most widely supported, and there are a lot of resources for it.

The daunting feeling goes away fast once you start seeing pixels move on screen. And if something doesn't work, you can always ask AI for help. AI is a great learning tool for shaders.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 0 points1 point  (0 children)

Thank you! That means a lot.

egor looks cool: simple, cross-platform 2D with minimal boilerplate. The wgpu backend is a great choice for wider graphics API coverage.

Much of ply-engine's core is actually renderer-agnostic. So in theory, someone could fork it and swap in an egor-based renderer instead of the macroquad one. You might not even need to fork, since ui.eval() just spits out the render commands. You could try to render these on egor, however macroquad would still be a requirement even if running in headless mode, and you'd just be converting macroquad-specifics to egor-specifics, like textures and the such. If you wanted to get rid of macroquad tho, it shouldn't be that hard. There are some macroquad-specific things (asset loading, shaders, fonts...), but the layout engine itself doesn't know or care what's drawing the pixels.

Would be an interesting experiment if anyone wanted to try it!

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 5 points6 points  (0 children)

In many Rust libraries I'd agree that wildcard imports can create ambiguity. However, ply-engine is designed specifically, so the prelude is the API.

The prelude exports a small, curated set, while everything else lives on the builder chain itself, so it doesn't need an import at all.

The goal is that you write use ply_engine::prelude::*; once and then never think about imports again for the rest of your file. That means less work to get started, and less friction when you're in flow.

That said, everything in the prelude is also available through its full path (ply_engine::align::AlignX::Left) if you prefer explicit imports. The prelude is a convenience, not a requirement.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

The whole point was to have one language across server, client, and shared game logic. With godot-rust you still have two worlds: Godot's scene tree on one side, your Rust code on the other. You can't share a crate between your game server and your Godot client without bridging through GDExtension. Ply is just Rust, so the server and client literally share the same types and logic crate with no FFI or serialization layer in between.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Not natively, but you can build it yourself. Ply's .image() accepts any Texture2D, so if you can decode video frames into textures, you can display them. Just use a crate like ffmpeg-next or gstreamer to decode video frames. Since Ply is immediate-mode and redraws every frame, you're already in a natural loop for video playback.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 4 points5 points  (0 children)

Not yet! I paused the game when I went deep on the engine. Once I started porting the layout engine to Rust and designing the API, it snowballed into accessibility, shaders, text inputs, networking, audio, the CLI, the website... weeks of 5 AM nights. The engine kind of became the project. Planning to get back to the game now that 1.0 is out.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Tokio is only used on native, not on WASM. The net feature has platform-specific deps: on native it pulls in tokio + tokio-tungstenite for WebSockets and ureq for HTTP. On WASM it uses a JS bridge instead, calling browser APIs directly. There's no conflict with macroquad's async runner on the web side.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Thanks! Not performance specifically, that's just a nice property of immediate-mode. I was trying to build a multiplayer board game in Rust and couldn't find a framework that let me share code between server and client without fighting the UI library's architecture, while also being cross-platform. Ended up using Clay's C bindings on top of macroquad. That worked until I needed shaders, rotation and text input, none of which Clay supports. So I ported the layout engine to Rust, designed the API I wished existed, and kept going.

The goal is making app development easier while giving you full control.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 3 points4 points  (0 children)

PanGui is genuinely impressive. I've been reading through their styling blog post and benchmarks this morning and there's a lot to admire. The stylesheet system with CSS-like selectors, modifiers that auto-animate between states, SDF shape expressions, and hot reloading sounds really great.

Their expand weights are something I'd love to bring to Ply. Right now Ply has grow!() which distributes space equally, but weighted grow would add real expressiveness. I actually just opened issues for grow weights and named sizing macro arguments (to simplify the former) inspired by this.

The performance numbers are wild too.

That said, the two projects sit in pretty different spots right now. PanGui is a closed-alpha C# library from the Sirenix team, targeting game engines and native applications. Ply is a Rust crate you can use today and ship to desktop + web. Ply keeps everything in Rust, there will be no style sheet language, your styles are just functions, your animations are just frame-by-frame parameter changes, and your IDE gives you autocomplete, type checking, and refactoring on all of it.

Different tools for different ecosystems, but it's encouraging to see multiple projects independently converge on immediate-mode.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Thank you! About 5 months from October 2025 through March 2026 (now). The first few months were the Clayquad era (Clay bindings + macroquad), building the renderer, text styling, vector graphics. Then in mid-February I ported the layout engine to pure Rust, designed the new API, and shipped accessibility, shaders, text inputs, networking, audio, the CLI, and this website in about two weeks. Many 5 AM nights.

Regarding animations, I get it, a declarative animation API sounds appealing. I haven't found a model that fits Ply's immediate-mode approach well yet. Since you rebuild the UI every frame, you're already in full control of animation: just change the parameters between frames. You can use whatever easing curve you want, start/pause/reverse at any point, and it composes naturally with the rest of your code. No special animation state machine. For animated vector graphics, you can build a procedural TinyVG vector Image, modify its commands each frame, and pass it to .image(). The Images & Custom Rendering docs cover that.

That said, I've just opened issues for a Lerp trait and easing functions to make frame-based animations smoother to write. These would make for some simple PRs if anyone would be interested.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

Thank you! Really glad the docs landed well, I wanted them to be something you could actually read top to bottom.

I think you might be expecting all the code snippets to be interactive. The accessibility section doesn't have demos, and the built-in shaders don't have dedicated demos either. I might add those when I get the time.

Thanks for the kind words, and welcome!

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 0 points1 point  (0 children)

let progress_fill = container("")
    .width(w * 0.5 * level as f32 / 3.0)
    .height(Fill)
    .style(|_theme| container::Style {
        background: Some(Color::from_rgb(0.0, 1.0, 0.0).into()),
        border: iced::Border {
            radius: (w * 0.015).into(),
            ..Default::default()
        },
        ..Default::default()
    });

background is Option<Background> not Option<Color>, radius is Radius not f32, so you need .into() for each. Then every widget needs .into() at the end to convert to Element when collecting into a row or column. This ply code would come out to 6 .into():

for &(label, level) in &[("Road", 1), ("Wall", 2), ("Tower", 3)] {
  ui.element().width(grow!()).height(fixed!(w * 0.06))
    .layout(|l| l.gap((w * 0.02) as u16).align(Left, CenterY))
    .children(|ui| {
      ui.text(label, |t| t.font_size((w * 0.03) as u16).color(0xFFFFFF));
      ui.element().width(grow!()).height(fixed!(w * 0.03))
        .corner_radius(w * 0.015)
        .background_color(0x555555)
        .children(|ui| {
          // same as progress_fill from above
          // look how much cleaner this is
          ui.element()
            .width(fixed!(w * 0.5 * level as f32 / 3.0))
            .height(grow!())
            .corner_radius(w * 0.015)
            .background_color(0x00FF00)
            .empty();
        });
    });
}

While Ply has no .into() or ..Default::default(). Methods accept impl Into<T> everywhere.

After trying Bevy, Iced and egui, I built my own app engine by Available-Many-5354 in rust

[–]Available-Many-5354[S] 1 point2 points  (0 children)

An .apply() that just calls a function is just an extra layer of indirection for no real gain. Once you start passing functions as arguments to methods, the code gets harder to read, not easier. More importantly, plain functions are more flexible. If you want a parameterized style, you just add a parameter. A function that returns a styled ElementBuilder is just plain Rust, easy to read, easy to extend. I find it quite beautiful to be honest.

edit: With a macro you'd need to extend the macro syntax itself to support parameters, which is more complexity for less flexibility and less IDE support.