This commit is contained in:
fwastring 2025-10-07 11:28:02 +02:00
commit cb6fa4450a
15 changed files with 3865 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2710
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

16
Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "confetti"
version = "0.1.0"
edition = "2024"
[dependencies]
bytemuck = "1.23.1"
env_logger = "0.11.8"
libloading = "0.8.8"
pollster = "0.4.0"
rand = "0.9.2"
raw-window-handle = "0.6.2"
smithay-client-toolkit = "0.19.2"
wayland-client = "0.31.11"
wgpu = "26.0.1"
winit = "0.30.12"

15
LICENSE Normal file
View file

@ -0,0 +1,15 @@
Creative Commons Attribution-NonCommercial 4.0 International License
Copyright (c) 2025 Sebastian Kootz
You are free to:
- Share: Copy and redistribute the material in any medium or format.
- Adapt: Remix, transform, and build upon the material for any purpose, non-commercially.
Under the following terms:
- Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
- Non-Commercial: You may not use the material for commercial purposes.
You can view the full text of the license here: https://creativecommons.org/licenses/by-nc/4.0/

158
README.md Normal file
View file

@ -0,0 +1,158 @@
# Sherlock Confetti
Sherlock Confetti is a lightweight program that displays a confetti animation
as a top-level overlay on Wayland compositors. It provides a fun and colorful
visual effect without interfering with your workflow.
<https://github.com/user-attachments/assets/d4e63a6b-ce41-4faa-8a1e-898570056e84>
## Features
- **Wayland-native:** Built specifically for Wayland environments.
- **Tiling Window Manager Friendly:** Displays on top without disrupting your
window layout.
- **High-performance rendering with WGPU:** Utilizes GPU acceleration for
smooth animation.
- **Shader-driven confetti:** Custom shaders deliver rich, dynamic visual
effects.
- **Multiple Color Palettes:** Choose from a variety of predefined vibrant and
pastel palettes.
- **Standalone or Integrated:** Originally created for the custom launcher
[Sherlock](https://github.com/Skxxtz/sherlock), but fully functional as a
standalone program.
## Usage
Simply run the program to enjoy confetti animations on your Wayland session. It
can be integrated or triggered by other applications such as launchers or
window managers.
```bash
confetti
```
## Installation
### Build Dependencies
- `libxkbcommon`
### Runtime Dependencies
- `vulkan-icd-loader`
- `mesa`
- `wayland`
- `wayland-protocols`
### <ins>From Source</ins>
To build Confetti from source, follow these steps.<br>
Make sure you have the necessary dependencies installed:
- `rust` - [How to install rust](https://www.rust-lang.org/tools/install)
- `git` - [How to install git](https://github.com/git-guides/install-git)
1. **Clone the repository**:
```bash
git clone https://github.com/skxxtz/sherlock-confetti.git
cd sherlock-confetti
```
2. **Build the project**:
```bash
cargo build --release
```
3. **Install the binary**:
After the build completes, install the binary to your system:
```bash
sudo cp target/release/confetti /usr/local/bin/
```
### <ins>From Binary</ins>
Make sure you have the following dependency installed:
- `tar` - [Tar](https://www.gnu.org/software/tar/)
1. **Download the archive containing the latest release**:
The archive can be found [here](https://github.com/Skxxtz/sherlock-confetti/releases/latest).
2. **Extract the files from the archive**:
```bash
cd ~/Downloads/
tar -xzf sherlock-confetti*.tar.gz
```
You can use tab-completion or run `ls` to verify the name of the archive.
3. **Install the binary**:
Now move the binary to a location on your `$PATH` environment variable:
```bash
sudo mv confetti /usr/local/bin/
```
Optionally also move the LICENSE file or delete it:
```bash
sudo mkdir -p /usr/share/doc/confetti
sudo mv LICENSE /usr/share/doc/confetti/
# or, to remove it:
rm LICENSE
```
### <ins>Build Debian Package</ins>
To build a `.deb` package directly from the source, follow these steps:<br>
Make sure you have the following dependencies installed:
- `rust` - [How to install rust](https://www.rust-lang.org/tools/install)
- `git` - [How to install git](https://github.com/git-guides/install-git)
1. **Install the `cargo-deb` tool**:
First, you need to install the `cargo-deb` tool, which simplifies packaging Rust projects as Debian packages:
```bash
cargo install cargo-deb
```
2. **Build the Debian package**:
After installing `cargo-deb`, run the following command to build the `.deb` package:
```bash
cargo deb
```
This will create a `.deb` package in the `target/debian` directory.
3. **Install the generated `.deb` package**:
Once the package is built, you can install it using:
```bash
sudo dpkg -i target/debian/confetti_*.deb
```
You can use tab-completion or `ls target/debian/` to confirm the file name.
(Make sure to replace the filename if the version number is different.)
## Palettes
Palettes can be selected by running `confetti` with the `--palette <name>`
flag. The names for available palettes are listed in the graphic below:
<div align="center" style="text-align:center; border-radius:10px;">
<picture>
<img alt="color palettes" width="100%" style="border-radius: 10px;" src="assets/palettes.png">
</picture>
</div>

BIN
assets/palettes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
assets/showcase.mp4 Normal file

Binary file not shown.

27
flake.lock generated Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1759733170,
"narHash": "sha256-TXnlsVb5Z8HXZ6mZoeOAIwxmvGHp1g4Dw89eLvIwKVI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8913c168d1c56dc49a7718685968f38752171c3b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

98
flake.nix Normal file
View file

@ -0,0 +1,98 @@
{
description = "Confetti packaged for Nix/NixOS";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# optional: pin a Rust toolchain via rust-overlay if you need nightly
# rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs =
{
self,
nixpkgs, # , rust-overlay
}:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; }));
in
{
packages = forAllSystems (
pkgs:
let
lib = pkgs.lib;
in
{
# name it default (and alias to confetti if you like)
default = pkgs.rustPlatform.buildRustPackage {
pname = "confetti";
version = "unstable";
src = ./.;
cargoHash = "sha256-6lwp5gky+NKhE2IKVI2Qxqe5HT9a8xqWfrJ2e9CK6IE="; # run once to get the real hash
# NOTE: makeWrapper is needed for wrapProgram
nativeBuildInputs = with pkgs; [
pkg-config
wayland-protocols
makeWrapper
];
# These are *runtime* libraries the app needs to load
buildInputs = with pkgs; [
wayland
libxkbcommon
vulkan-loader
mesa
];
# Ensure the Wayland/Vulkan/libxkbcommon libs are found at runtime
postInstall =
let
libPath = lib.makeLibraryPath [
pkgs.wayland
pkgs.libxkbcommon
pkgs.vulkan-loader
pkgs.mesa
];
in
''
wrapProgram "$out/bin/confetti" \
--prefix LD_LIBRARY_PATH : ${libPath}
'';
};
confetti = self.packages.${pkgs.system}.default;
}
);
# Expose a runnable `nix run`
apps = forAllSystems (pkgs: {
default = {
type = "app";
program = "${self.packages.${pkgs.system}.default}/bin/confetti";
};
});
# Nice dev shell for hacking locally (cargo, rustc, system libs)
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
pkg-config
rustc
cargo
# or: (rust-bin.stable.latest.default) if using rust-overlay
wayland-protocols
];
buildInputs = with pkgs; [
wayland
libxkbcommon
vulkan-loader
mesa
];
};
});
};
}

11
packager.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash
read -p "Current version: " version
rm -rf ~/.tmp/confetti-release/
mkdir -p ~/.tmp/confetti-release/
cargo build --release
cp target/release/confetti ~/.tmp/confetti-release/
cp LICENSE ~/.tmp/confetti-release/LICENSE
cd ~/.tmp/confetti-release/
tar -czf confetti-v${version}-bin-linux-x86_64.tar.gz confetti LICENSE

1
result Symbolic link
View file

@ -0,0 +1 @@
/nix/store/rqczppgcgsbvkbi80hvrxgz3aq9byyj8-confetti-unstable

169
src/color_palette.rs Normal file
View file

@ -0,0 +1,169 @@
pub enum ColorPalette {
Party,
Pastel,
Earth,
Neon,
Cool,
Sunset,
Ocean,
Retro,
Forest,
Candy,
}
type ColorVec = Vec<[f32; 3]>;
impl ColorPalette {
pub fn get_colors(&self) -> Vec<[f32; 3]> {
match &self {
Self::Party => Self::party(),
Self::Pastel => Self::pastel(),
Self::Earth => Self::earth_tones(),
Self::Neon => Self::neon(),
Self::Cool => Self::cool(),
Self::Sunset => Self::sunset(),
Self::Ocean => Self::ocean(),
Self::Retro => Self::retro(),
Self::Forest => Self::forest(),
Self::Candy => Self::candy(),
}
}
fn party() -> ColorVec {
vec![
[1.0, 0.0, 0.0], // bright red
[1.0, 0.5, 0.0], // vivid orange
[1.0, 1.0, 0.0], // bright yellow
[0.0, 1.0, 0.0], // neon green
[0.0, 1.0, 1.0], // bright cyan
[0.0, 0.0, 1.0], // electric blue
[0.7, 0.0, 1.0], // vibrant purple
[1.0, 0.0, 1.0], // hot pink
[1.0, 0.2, 0.5], // neon pink
[1.0, 0.8, 0.0], // gold yellow
]
}
fn pastel() -> ColorVec {
vec![
[1.0, 0.8, 0.8], // pastel pink
[0.8, 1.0, 0.8], // pastel green
[0.8, 0.8, 1.0], // pastel blue
[1.0, 0.9, 0.7], // cream
[0.9, 0.8, 1.0], // lavender
[1.0, 0.85, 0.85], // light coral
[0.85, 1.0, 0.85], // mint
[0.85, 0.85, 1.0], // light periwinkle
]
}
fn neon() -> ColorVec {
vec![
[1.0, 0.1, 0.1], // neon red
[1.0, 0.5, 0.0], // neon orange
[1.0, 1.0, 0.0], // neon yellow
[0.0, 1.0, 0.0], // neon green
[0.0, 1.0, 1.0], // neon cyan
[0.0, 0.1, 1.0], // neon blue
[0.6, 0.0, 1.0], // neon purple
[1.0, 0.0, 1.0], // neon magenta
]
}
fn earth_tones() -> ColorVec {
vec![
[0.5, 0.3, 0.1], // brown
[0.6, 0.4, 0.2], // tan
[0.4, 0.5, 0.3], // olive green
[0.2, 0.3, 0.1], // dark olive
[0.8, 0.7, 0.5], // sand
[0.3, 0.2, 0.1], // dark brown
]
}
fn cool() -> ColorVec {
vec![
[0.0, 0.5, 1.0], // sky blue
[0.0, 0.7, 0.9], // turquoise
[0.0, 0.4, 0.6], // teal
[0.3, 0.6, 0.8], // steel blue
[0.2, 0.3, 0.5], // navy
[0.5, 0.7, 0.9], // light blue
]
}
fn sunset() -> ColorVec {
vec![
[1.0, 0.4, 0.0], // orange
[1.0, 0.7, 0.4], // light orange
[0.9, 0.2, 0.3], // deep pink
[0.6, 0.0, 0.3], // maroon
[0.9, 0.5, 0.1], // gold
[1.0, 0.3, 0.0], // fiery red-orange
]
}
fn ocean() -> ColorVec {
vec![
[0.0, 0.5, 0.7], // deep sea blue
[0.0, 0.7, 0.9], // aqua
[0.2, 0.8, 0.8], // light teal
[0.0, 0.3, 0.5], // navy blue
[0.1, 0.6, 0.8], // sky blue
[0.3, 0.9, 1.0], // bright cyan
]
}
fn retro() -> ColorVec {
vec![
[1.0, 0.3, 0.5], // pink
[1.0, 0.6, 0.0], // orange
[0.9, 0.8, 0.2], // mustard yellow
[0.3, 0.7, 0.6], // teal
[0.6, 0.3, 0.6], // purple
[0.8, 0.4, 0.2], // burnt sienna
]
}
fn forest() -> ColorVec {
vec![
[0.0, 0.3, 0.0], // dark green
[0.1, 0.5, 0.1], // moss green
[0.2, 0.6, 0.2], // pine green
[0.4, 0.8, 0.4], // leaf green
[0.1, 0.4, 0.1], // olive green
[0.3, 0.5, 0.3], // fern green
]
}
fn candy() -> ColorVec {
vec![
[1.0, 0.7, 0.8], // cotton candy pink
[1.0, 0.9, 0.6], // pale yellow
[0.8, 1.0, 0.7], // light lime
[0.7, 0.8, 1.0], // baby blue
[1.0, 0.6, 0.7], // bubblegum pink
[0.9, 0.7, 1.0], // lavender pink
]
}
}
impl std::str::FromStr for ColorPalette {
type Err = String; // or a custom error type
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"party" => Ok(ColorPalette::Party),
"pastel" => Ok(ColorPalette::Pastel),
"earth" => Ok(ColorPalette::Earth),
"neon" => Ok(ColorPalette::Neon),
"cool" => Ok(ColorPalette::Cool),
"sunset" => Ok(ColorPalette::Sunset),
"ocean" => Ok(ColorPalette::Ocean),
"retro" => Ok(ColorPalette::Retro),
"forest" => Ok(ColorPalette::Forest),
"candy" => Ok(ColorPalette::Candy),
_ => Err(format!("Unknown color pallette: {}", s)),
}
}
}
impl Default for ColorPalette {
fn default() -> Self {
Self::Retro
}
}

181
src/implementations.rs Normal file
View file

@ -0,0 +1,181 @@
use std::{num::NonZeroU32, time::Duration};
use smithay_client_toolkit::{
compositor::CompositorHandler,
delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_seat,
output::{OutputHandler, OutputState},
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::{Capability, SeatHandler, SeatState},
shell::wlr_layer::{LayerShellHandler, LayerSurface, LayerSurfaceConfigure},
};
use wayland_client::{
Connection, QueueHandle,
protocol::{wl_output, wl_seat, wl_surface},
};
use crate::Wgpu;
delegate_compositor!(Wgpu);
delegate_output!(Wgpu);
delegate_seat!(Wgpu);
delegate_layer!(Wgpu);
delegate_registry!(Wgpu);
impl LayerShellHandler for Wgpu {
fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
self.exit = true;
}
fn configure(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
_layer: &LayerSurface,
configure: LayerSurfaceConfigure,
_serial: u32,
) {
self.width = NonZeroU32::new(configure.new_size.0).map_or(256, NonZeroU32::get);
self.height = NonZeroU32::new(configure.new_size.1).map_or(256, NonZeroU32::get);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: wgpu::TextureFormat::Bgra8Unorm,
width: self.width,
height: self.height,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied,
view_formats: vec![],
desired_maximum_frame_latency: 0,
};
// Initiate the first draw.
if self.first_configure {
self.first_configure = false;
self.surface.configure(&self.device, &config);
}
loop {
if self.exit {
break;
}
self.draw(qh);
std::thread::sleep(Duration::from_millis(16));
}
}
}
impl CompositorHandler for Wgpu {
fn scale_factor_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_new_factor: i32,
) {
// Not needed for this example.
}
fn transform_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_new_transform: wl_output::Transform,
) {
// Not needed for this example.
}
fn frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_time: u32,
) {
}
fn surface_enter(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
// Not needed for this example.
}
fn surface_leave(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
// Not needed for this example.
}
}
impl OutputHandler for Wgpu {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
fn update_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
fn output_destroyed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
}
impl SeatHandler for Wgpu {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
fn new_capability(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_seat: wl_seat::WlSeat,
_capability: Capability,
) {
}
fn remove_capability(
&mut self,
_conn: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
_capability: Capability,
) {
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
}
impl ProvidesRegistryState for Wgpu {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState];
}

438
src/main.rs Normal file
View file

@ -0,0 +1,438 @@
use rand::Rng;
use raw_window_handle::{
RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
};
use smithay_client_toolkit::{
compositor::CompositorState,
output::OutputState,
registry::RegistryState,
seat::SeatState,
shell::{
WaylandSurface,
wlr_layer::{Anchor, LayerShell, LayerSurface},
},
};
use std::{borrow::Cow, env::args, ptr::NonNull, str::FromStr, time::Instant};
use wayland_client::{Connection, Proxy, QueueHandle, globals::registry_queue_init};
use wgpu::{BindGroup, Buffer, util::DeviceExt};
use crate::color_palette::ColorPalette;
mod color_palette;
mod implementations;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 2],
}
impl Vertex {
#[allow(dead_code)]
fn triangle(x: f32, y: f32, w: f32, h: f32) -> [Self; 3] {
let x0 = x;
let x1 = x + w;
let y0 = y;
let y1 = y + h;
[
Vertex { position: [x0, y0] },
Vertex { position: [x1, y0] },
Vertex { position: [x1, y1] },
]
}
#[allow(dead_code)]
fn rectangle(x: f32, y: f32, w: f32, h: f32) -> [Self; 6] {
let x0 = x;
let x1 = x + w;
let y0 = y;
let y1 = y + h;
[
Vertex { position: [x0, y0] }, // Triangle 1
Vertex { position: [x1, y0] },
Vertex { position: [x1, y1] },
Vertex { position: [x0, y0] }, // Triangle 2
Vertex { position: [x1, y1] },
Vertex { position: [x0, y1] },
]
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct InstanceData {
direction: [f32; 2],
color: [f32; 3],
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
time: f32,
}
impl Uniforms {
fn new() -> Self {
Self { time: 0.0 }
}
}
fn main() {
env_logger::init();
let conn = Connection::connect_to_env().unwrap();
let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
let qh = event_queue.handle();
// Initialize xdg_shell handlers so we can select the correct adapter
let compositor_state =
CompositorState::bind(&globals, &qh).expect("wl_compositor not available");
let layer_state = LayerShell::bind(&globals, &qh).expect("layer_shell not available");
let surface = compositor_state.create_surface(&qh);
// Create the window for adapter selection
let layer = layer_state.create_layer_surface(
&qh,
surface,
smithay_client_toolkit::shell::wlr_layer::Layer::Top,
Some(""),
None,
);
layer.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
let (width, height) = (400, 400);
layer.set_size(0, 0); // 0 width = stretch to full width
layer.set_opaque_region(None);
layer.commit();
// Initialize wgpu
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
// Create the raw window handle for the surface.
let raw_display_handle = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(
NonNull::new(conn.backend().display_ptr() as *mut _).unwrap(),
));
let raw_window_handle = RawWindowHandle::Wayland(WaylandWindowHandle::new(
NonNull::new(layer.wl_surface().id().as_ptr() as *mut _).unwrap(),
));
let surface = unsafe {
instance
.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
raw_display_handle,
raw_window_handle,
})
.unwrap()
};
// Pick a supported adapter
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
}))
.expect("Failed to find suitable adapter");
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))
.expect("Failed to request device");
let mut surface_config = surface.get_default_config(&adapter, 200, 200).unwrap();
surface_config.alpha_mode = wgpu::CompositeAlphaMode::PreMultiplied;
surface_config.format = wgpu::TextureFormat::Bgra8Unorm;
let (layout, group, uniform_buffer, uniforms) = create_uniforms(&device);
let (vertex_buffer, instance_buffer, vertex_count, instance_count) =
create_vertex_buffer(&device, width as f32, height as f32);
let render_pipeline = create_pipeline(&device, surface_config.format, &layout);
let mut wgpu = Wgpu {
registry_state: RegistryState::new(&globals),
seat_state: SeatState::new(&globals, &qh),
output_state: OutputState::new(&globals, &qh),
start_time: Instant::now(),
first_configure: true,
exit: false,
width: 256,
height: 256,
window: layer,
device,
surface,
queue,
render_pipeline,
group,
uniforms,
uniform_buffer,
vertex_buffer,
instance_buffer,
vertex_count,
instance_count,
};
// We don't draw immediately, the configure will notify us when to first draw.
loop {
event_queue.blocking_dispatch(&mut wgpu).unwrap();
if wgpu.exit {
break;
}
}
// On exit we must destroy the surface before the window is destroyed.
drop(wgpu.surface);
drop(wgpu.window);
}
struct Wgpu {
registry_state: RegistryState,
seat_state: SeatState,
output_state: OutputState,
start_time: Instant,
exit: bool,
first_configure: bool,
width: u32,
height: u32,
window: LayerSurface,
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
render_pipeline: wgpu::RenderPipeline,
group: wgpu::BindGroup,
uniforms: Uniforms,
uniform_buffer: Buffer,
vertex_buffer: Buffer,
instance_buffer: Buffer,
vertex_count: u32,
instance_count: u32,
}
impl Wgpu {
fn draw(&mut self, _qh: &QueueHandle<Self>) {
let elapsed = self.start_time.elapsed().as_secs_f32();
self.update_time(elapsed);
if elapsed > 3.0 {
self.exit = true
}
let surface_texture = self
.surface
.get_current_texture()
.expect("Failed to acquire next swap chain texture");
let texture_view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_bind_group(0, &self.group, &[]);
rpass.set_pipeline(&self.render_pipeline);
rpass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
rpass.set_vertex_buffer(1, self.instance_buffer.slice(..));
rpass.draw(0..self.vertex_count, 0..self.instance_count);
}
self.queue.submit(Some(encoder.finish()));
surface_texture.present();
}
pub fn update_time(&mut self, time: f32) {
self.uniforms.time = time;
self.queue
.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&self.uniforms));
}
}
fn create_pipeline(
device: &wgpu::Device,
swap_chain_format: wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
// Load theushaders from disk
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[bind_group_layout],
push_constant_ranges: &[],
});
let vertex_buffer_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
}],
};
let instance_buffer_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<InstanceData>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2, // v_start
offset: 0,
shader_location: 1,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3, // color
offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
shader_location: 2,
},
],
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[vertex_buffer_layout, instance_buffer_layout],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: swap_chain_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
// strip_index_format: None,
// front_face: wgpu::FrontFace::Ccw,
// cull_mode: Some(wgpu::Face::Back),
// // Setting this to anything other than Fill requires Features::POLYGON_MODE_LINE
// // or Features::POLYGON_MODE_POINT
// polygon_mode: wgpu::PolygonMode::Fill,
// // Requires Features::DEPTH_CLIP_CONTROL
// unclipped_depth: false,
// // Requires Features::CONSERVATIVE_RASTERIZATION
// conservative: false,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
})
}
fn create_uniforms(device: &wgpu::Device) -> (wgpu::BindGroupLayout, BindGroup, Buffer, Uniforms) {
let uniforms = Uniforms::new();
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Uniform Buffer"),
contents: bytemuck::bytes_of(&uniforms),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("uniform_bind_group_layout"),
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
label: Some("uniform_bind_group"),
});
(
uniform_bind_group_layout,
uniform_bind_group,
uniform_buffer,
uniforms,
)
}
fn create_vertex_buffer(
device: &wgpu::Device,
width: f32,
height: f32,
) -> (Buffer, Buffer, u32, u32) {
// Only 1 rectangle vertices here, since instances define position:
let rectangle = Vertex::rectangle(0.0, 0.0, 0.00002 * height, 0.00002 * width);
let args = args().collect::<Vec<String>>();
let pallette = extreact_flag_value::<ColorPalette>(&args, "--pallette").unwrap_or_default();
let colors = pallette.get_colors();
let color_count = colors.len();
let mut rng = rand::rng();
let instances = (0..200)
.map(|_| {
let x = rng.random_range(-1.0..1.0) as f32;
let y_max = (1.0 - x * x).sqrt() * 2.5;
let y = rng.random_range(-0.5..y_max);
InstanceData {
direction: [x * 1.2, y],
color: colors
.get(rng.random_range(0..color_count))
.unwrap()
.clone(),
}
})
.collect::<Vec<InstanceData>>();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Rectangle Vertex Buffer"),
contents: bytemuck::cast_slice(&rectangle),
usage: wgpu::BufferUsages::VERTEX,
});
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Instance Buffer"),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX,
});
(
vertex_buffer,
instance_buffer,
rectangle.len() as u32,
instances.len() as u32,
)
}
fn extreact_flag_value<T: FromStr>(args: &Vec<String>, name: &str) -> Option<T> {
let pos = args.iter().position(|arg| arg == name)?;
let val = args.get(pos + 1)?;
if val.starts_with("-") {
return None;
}
val.parse::<T>().ok()
}

40
src/shader.wgsl Normal file
View file

@ -0,0 +1,40 @@
struct Uniforms {
time: f32,
};
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) v_start: vec2<f32>,
@location(2) color: vec3<f32>, // Add color if you want per-vertex or per-instance colors
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>, // Pass color to fragment shader
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
var<private> g: vec2<f32> = vec2<f32>(0.0, -0.2);
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
let t = uniforms.time;
let decay = 0.01;
let new_x = clamp(input.position.x + input.v_start.x * t - t * decay, -1.0, 1.0);
let new_y = input.position.y + input.v_start.y * t - t * t;
var output: VertexOutput;
output.position = vec4<f32>(new_x, new_y, 0.0, 1.0);
output.color = input.color;
return output;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(input.color, 1.0);
}