Compare commits
6 Commits
a99bf9c17b
...
8097ff725b
| Author | SHA1 | Date | |
|---|---|---|---|
| 8097ff725b | |||
| 330560f79b | |||
| a153a3f04a | |||
| d91fd0f4dc | |||
| b827c759ba | |||
| 045617b763 |
23
assets/impact-sounds/License.txt
Normal file
23
assets/impact-sounds/License.txt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
Impact Sounds (1.0)
|
||||||
|
|
||||||
|
Created/distributed by Kenney (www.kenney.nl)
|
||||||
|
Creation date: 19-12-2019
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
License: (Creative Commons Zero, CC0)
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
|
||||||
|
This content is free to use in personal, educational and commercial projects.
|
||||||
|
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Donate: http://support.kenney.nl
|
||||||
|
Request: http://request.kenney.nl
|
||||||
|
Patreon: http://patreon.com/kenney/
|
||||||
|
|
||||||
|
Follow on Twitter for updates:
|
||||||
|
http://twitter.com/KenneyNL
|
||||||
BIN
assets/impact-sounds/footstep_concrete_000.ogg
Normal file
BIN
assets/impact-sounds/footstep_concrete_000.ogg
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="0" nextlayerid="5" nextobjectid="1">
|
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="0" nextlayerid="6" nextobjectid="4">
|
||||||
<tileset firstgid="1" source="tileset.tsx"/>
|
<tileset firstgid="1" source="tileset.tsx"/>
|
||||||
<layer id="2" name="Ground" width="30" height="20">
|
<layer id="2" name="Ground" width="30" height="20">
|
||||||
<data encoding="csv">
|
<data encoding="csv">
|
||||||
@ -49,7 +49,7 @@
|
|||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
</data>
|
</data>
|
||||||
</layer>
|
</layer>
|
||||||
<layer id="4" name="Pushable" width="30" height="20">
|
<layer id="4" name="Boxes" width="30" height="20">
|
||||||
<data encoding="csv">
|
<data encoding="csv">
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
@ -73,4 +73,9 @@
|
|||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
</data>
|
</data>
|
||||||
</layer>
|
</layer>
|
||||||
|
<objectgroup id="5" name="Markers">
|
||||||
|
<object id="1" name="spawn" x="479" y="550">
|
||||||
|
<point/>
|
||||||
|
</object>
|
||||||
|
</objectgroup>
|
||||||
</map>
|
</map>
|
||||||
|
|||||||
@ -1,17 +1,31 @@
|
|||||||
{
|
{
|
||||||
"activeFile": "",
|
"activeFile": "map.tmx",
|
||||||
"expandedProjectPaths": [
|
"expandedProjectPaths": [
|
||||||
],
|
],
|
||||||
|
"file.lastUsedOpenFilter": "All Files (*)",
|
||||||
"fileStates": {
|
"fileStates": {
|
||||||
|
"map.tmx": {
|
||||||
|
"scale": 1,
|
||||||
|
"selectedLayer": 3,
|
||||||
|
"viewCenter": {
|
||||||
|
"x": 624.5,
|
||||||
|
"y": 421
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tileset.tsx": {
|
||||||
|
"scaleInDock": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban",
|
"last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban",
|
||||||
"map.lastUsedFormat": "tmx",
|
"map.lastUsedFormat": "tmx",
|
||||||
"map.tileHeight": 64,
|
"map.tileHeight": 64,
|
||||||
"map.tileWidth": 64,
|
"map.tileWidth": 64,
|
||||||
"openFiles": [
|
"openFiles": [
|
||||||
|
"map.tmx"
|
||||||
],
|
],
|
||||||
"project": "sokoban.tiled-project",
|
"project": "sokoban.tiled-project",
|
||||||
"recentFiles": [
|
"recentFiles": [
|
||||||
|
"map.tmx"
|
||||||
],
|
],
|
||||||
"tileset.lastUsedFormat": "tsx",
|
"tileset.lastUsedFormat": "tsx",
|
||||||
"tileset.tileSize": {
|
"tileset.tileSize": {
|
||||||
|
|||||||
@ -10,7 +10,8 @@ pub fn build(b: *std.Build) !void {
|
|||||||
"sokoban/sokoban_tilesheet.png",
|
"sokoban/sokoban_tilesheet.png",
|
||||||
"roboto-font/Roboto-Regular.ttf",
|
"roboto-font/Roboto-Regular.ttf",
|
||||||
"tiled/map.tmx",
|
"tiled/map.tmx",
|
||||||
"tiled/tileset.tsx"
|
"tiled/tileset.tsx",
|
||||||
|
"impact-sounds/footstep_concrete_000.ogg"
|
||||||
};
|
};
|
||||||
|
|
||||||
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });
|
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });
|
||||||
|
|||||||
@ -98,7 +98,16 @@ pub const ObjectVariant = struct {
|
|||||||
|
|
||||||
color: ?Color,
|
color: ?Color,
|
||||||
draw_order: DrawOrder,
|
draw_order: DrawOrder,
|
||||||
items: []Object
|
items: []Object,
|
||||||
|
|
||||||
|
pub fn getByName(self: *const ObjectVariant, name: []const u8) ?*const Object {
|
||||||
|
for (self.items) |*item| {
|
||||||
|
if (std.mem.eql(u8, item.name, name)) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const GroupVariant = struct {
|
pub const GroupVariant = struct {
|
||||||
|
|||||||
@ -77,6 +77,14 @@ pub const Tile = struct {
|
|||||||
pub fn getProperties(self: Tile) Property.List {
|
pub fn getProperties(self: Tile) Property.List {
|
||||||
return self.tileset.getTileProperties(self.id) orelse .empty;
|
return self.tileset.getTileProperties(self.id) orelse .empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getPositionInImage(self: Tile) Position {
|
||||||
|
return self.tileset.getTilePositionInImage(self.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPosition(self: Tile) Tileset.TilePosition {
|
||||||
|
return self.tileset.getTilePosition(self.id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
@ -230,6 +238,72 @@ pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getLayer(self: *const Tilemap, name: []const u8) ?*const Layer {
|
||||||
|
for (self.layers) |*layer| {
|
||||||
|
if (std.mem.eql(u8, layer.name, name)) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Bounds = struct {
|
||||||
|
min_x: usize,
|
||||||
|
min_y: usize,
|
||||||
|
max_x: usize,
|
||||||
|
max_y: usize,
|
||||||
|
|
||||||
|
const zero = Bounds{
|
||||||
|
.min_x = 0,
|
||||||
|
.min_y = 0,
|
||||||
|
.max_x = 0,
|
||||||
|
.max_y = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn width(self: Bounds) usize {
|
||||||
|
return self.max_x - self.min_x + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(self: Bounds) usize {
|
||||||
|
return self.max_y - self.min_y + 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getTileBounds(self: *const Tilemap) Bounds {
|
||||||
|
var result: ?Bounds = null;
|
||||||
|
|
||||||
|
for (self.layers) |*layer| {
|
||||||
|
if (layer.variant != .tile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0..self.height) |y| {
|
||||||
|
for (0..self.width) |x| {
|
||||||
|
const gid = layer.variant.tile.get(x, y).?;
|
||||||
|
if (gid != 0) {
|
||||||
|
if (result) |bounds| {
|
||||||
|
result = Bounds{
|
||||||
|
.min_x = @min(x, bounds.min_x),
|
||||||
|
.min_y = @min(y, bounds.min_y),
|
||||||
|
.max_x = @max(x, bounds.max_x),
|
||||||
|
.max_y = @max(y, bounds.max_y)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
result = Bounds{
|
||||||
|
.min_x = x,
|
||||||
|
.min_y = y,
|
||||||
|
.max_x = x,
|
||||||
|
.max_y = y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result orelse Bounds.zero;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *const Tilemap) void {
|
pub fn deinit(self: *const Tilemap) void {
|
||||||
self.arena.deinit();
|
self.arena.deinit();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Io = std.Io;
|
const Io = std.Io;
|
||||||
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const xml = @import("./xml.zig");
|
const xml = @import("./xml.zig");
|
||||||
@ -204,21 +205,32 @@ pub fn getTileProperties(self: *const Tileset, id: u32) ?Property.List {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getTilePositionInImage(self: *const Tileset, id: u32) ?Position {
|
pub const TilePosition = struct {
|
||||||
if (id >= self.tile_count) {
|
x: usize,
|
||||||
return null;
|
y: usize,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
pub fn getTilePosition(self: *const Tileset, id: u32) TilePosition {
|
||||||
|
assert(id < self.tile_count);
|
||||||
|
|
||||||
const tileset_width = @divExact(self.image.width, self.tile_width);
|
const tileset_width = @divExact(self.image.width, self.tile_width);
|
||||||
|
|
||||||
const tile_x = @mod(id, tileset_width);
|
const tile_x = @mod(id, tileset_width);
|
||||||
const tile_y = @divFloor(id, tileset_width);
|
const tile_y = @divFloor(id, tileset_width);
|
||||||
|
|
||||||
return Position{
|
return .{
|
||||||
.x = @floatFromInt(tile_x * self.tile_width),
|
.x = tile_x,
|
||||||
.y = @floatFromInt(tile_y * self.tile_height),
|
.y = tile_y,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTilePositionInImage(self: *const Tileset, id: u32) Position {
|
||||||
|
const pos = self.getTilePosition(id);
|
||||||
|
|
||||||
|
return Position{
|
||||||
|
.x = @floatFromInt(pos.x * self.tile_width),
|
||||||
|
.y = @floatFromInt(pos.y * self.tile_height),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *const Tileset) void {
|
pub fn deinit(self: *const Tileset) void {
|
||||||
|
|||||||
310
src/app.zig
310
src/app.zig
@ -1,5 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Math = @import("math");
|
const Math = @import("math");
|
||||||
@ -40,20 +41,75 @@ const PlayerSprites = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
show_first_window: bool,
|
const Tilemap = struct {
|
||||||
check: bool,
|
tiles: []Tile,
|
||||||
player_pos: Vec2,
|
width: usize,
|
||||||
wall_sprite: Gfx.Sprite.Id,
|
height: usize,
|
||||||
|
|
||||||
|
const Tile = struct {
|
||||||
|
ground_sprite: ?Gfx.Sprite.Id,
|
||||||
|
wall_sprite: ?Gfx.Sprite.Id,
|
||||||
|
|
||||||
|
const empty = Tile{
|
||||||
|
.ground_sprite = null,
|
||||||
|
.wall_sprite = null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(gpa: Allocator, width: usize, height: usize) !Tilemap {
|
||||||
|
const tiles = try gpa.alloc(Tile, width * height);
|
||||||
|
@memset(tiles, .empty);
|
||||||
|
|
||||||
|
return Tilemap{
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.tiles = tiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Tilemap, gpa: Allocator) void {
|
||||||
|
gpa.free(self.tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *Tilemap, x: usize, y: usize) *Tile {
|
||||||
|
assert(x < self.width);
|
||||||
|
assert(y < self.height);
|
||||||
|
return &self.tiles[y * self.width + x];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Entity = struct {
|
||||||
|
type: Type,
|
||||||
|
|
||||||
|
pos_x: i32 = 0,
|
||||||
|
pos_y: i32 = 0,
|
||||||
|
sprite: ?Gfx.Sprite.Id = null,
|
||||||
|
|
||||||
|
player_direction: Direction = .down,
|
||||||
|
player_frame_index: u32 = 0,
|
||||||
|
|
||||||
|
const Type = enum {
|
||||||
|
player,
|
||||||
|
box
|
||||||
|
};
|
||||||
|
|
||||||
|
const SlotMap = Platform.SlotMapType(u8, u16, Entity);
|
||||||
|
const Id = SlotMap.Id;
|
||||||
|
};
|
||||||
|
|
||||||
roboto_font: Gfx.Font.Id,
|
roboto_font: Gfx.Font.Id,
|
||||||
|
tilemap: Tilemap,
|
||||||
|
entities: Entity.SlotMap,
|
||||||
|
|
||||||
tilesheet: Tilesheet,
|
tilesheet: Tilesheet,
|
||||||
|
|
||||||
player_sprites: PlayerSprites,
|
player_sprites: PlayerSprites,
|
||||||
player_direction: Direction,
|
|
||||||
player_frame_index: u32,
|
|
||||||
animation_timer: Platform.Nanoseconds,
|
animation_timer: Platform.Nanoseconds,
|
||||||
|
|
||||||
sound: Audio.Sound.Id,
|
footstep_buffer: Audio.BufferId,
|
||||||
|
footstep_sound: Audio.SoundId = .nil,
|
||||||
|
|
||||||
|
sfx_bus: Audio.BusId,
|
||||||
|
|
||||||
const Tilesheet = struct {
|
const Tilesheet = struct {
|
||||||
image: Gfx.ImageData,
|
image: Gfx.ImageData,
|
||||||
@ -62,7 +118,9 @@ const Tilesheet = struct {
|
|||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
|
||||||
pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet {
|
pub fn init(gpa: Allocator, image: Gfx.ImageData, tile_width: u32, tile_height: u32) !Tilesheet {
|
||||||
|
const width = image.width / tile_width;
|
||||||
|
const height = image.height / tile_height;
|
||||||
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
|
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
|
||||||
errdefer gpa.free(sprites);
|
errdefer gpa.free(sprites);
|
||||||
@memset(sprites, .nil);
|
@memset(sprites, .nil);
|
||||||
@ -72,7 +130,7 @@ const Tilesheet = struct {
|
|||||||
.image = image,
|
.image = image,
|
||||||
.width = width,
|
.width = width,
|
||||||
.height = height,
|
.height = height,
|
||||||
.tile_size = tile_size
|
.tile_size = .initFromInt(u32, tile_width, tile_height)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +140,7 @@ const Tilesheet = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: Tilesheet, x: u32, y: u32) Gfx.Sprite.Id {
|
pub fn get(self: Tilesheet, x: usize, y: usize) Gfx.Sprite.Id {
|
||||||
if (x >= self.width or y >= self.height) {
|
if (x >= self.width or y >= self.height) {
|
||||||
log.warn("Attempt to get tile which is out of bounds", .{});
|
log.warn("Attempt to get tile which is out of bounds", .{});
|
||||||
return .nil;
|
return .nil;
|
||||||
@ -106,24 +164,6 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
|||||||
const robot_font = Gfx.initFont();
|
const robot_font = Gfx.initFont();
|
||||||
Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
|
Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
|
||||||
|
|
||||||
var scratch = std.heap.ArenaAllocator.init(plt.gpa);
|
|
||||||
defer scratch.deinit();
|
|
||||||
|
|
||||||
var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa);
|
|
||||||
defer buffers.deinit();
|
|
||||||
|
|
||||||
const tilemap = try Tiled.Tilemap.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/map.tmx"));
|
|
||||||
defer tilemap.deinit();
|
|
||||||
|
|
||||||
var tilesets: Tiled.Tileset.List = .empty;
|
|
||||||
defer tilesets.deinit(plt.gpa);
|
|
||||||
|
|
||||||
try tilesets.add(plt.gpa, "tiled/tilemap.tsx", try Tiled.Tileset.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/tileset.tsx")));
|
|
||||||
|
|
||||||
for (tilemap.layers) |layer| {
|
|
||||||
std.debug.print("{s}\n", .{layer.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png"));
|
const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png"));
|
||||||
const tilesheet = try Tilesheet.init(
|
const tilesheet = try Tilesheet.init(
|
||||||
plt.arena,
|
plt.arena,
|
||||||
@ -132,13 +172,9 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
|||||||
.height = tilesheet_png.height,
|
.height = tilesheet_png.height,
|
||||||
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels }
|
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels }
|
||||||
},
|
},
|
||||||
13,
|
64, 64,
|
||||||
8,
|
|
||||||
.init(64, 64)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const wall_sprite = tilesheet.get(1, 0);
|
|
||||||
|
|
||||||
var player_sprites: PlayerSprites = undefined;
|
var player_sprites: PlayerSprites = undefined;
|
||||||
inline for (.{
|
inline for (.{
|
||||||
.{
|
.{
|
||||||
@ -164,99 +200,203 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sound = Audio.initSound(.{
|
const footstep_sound = Audio.addBuffer(.{
|
||||||
.cb = .{ .sample = sinSampleCallback },
|
.vorbis = plt.assets.readFile("impact-sounds/footstep_concrete_000.ogg")
|
||||||
.playing = false,
|
|
||||||
.frequency = 1
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.* = App{
|
self.* = App{
|
||||||
.player_pos = .init(100, 100),
|
|
||||||
.show_first_window = true,
|
|
||||||
.check = false,
|
|
||||||
.player_sprites = player_sprites,
|
.player_sprites = player_sprites,
|
||||||
.wall_sprite = wall_sprite,
|
|
||||||
.roboto_font = robot_font,
|
.roboto_font = robot_font,
|
||||||
.player_direction = .left,
|
|
||||||
.player_frame_index = 0,
|
|
||||||
.animation_timer = 0,
|
.animation_timer = 0,
|
||||||
.tilesheet = tilesheet,
|
.tilesheet = tilesheet,
|
||||||
.sound = sound
|
.footstep_buffer = footstep_sound,
|
||||||
|
.sfx_bus = Audio.addBus(.{ .label = "sfx" }),
|
||||||
|
.tilemap = undefined,
|
||||||
|
.entities = try .initCapacity(plt.gpa, 256)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var player_pos_x: i32 = 0;
|
||||||
|
var player_pos_y: i32 = 0;
|
||||||
|
{
|
||||||
|
var scratch = std.heap.ArenaAllocator.init(plt.gpa);
|
||||||
|
defer scratch.deinit();
|
||||||
|
|
||||||
|
var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa);
|
||||||
|
defer buffers.deinit();
|
||||||
|
|
||||||
|
const tilemap = try Tiled.Tilemap.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/map.tmx"));
|
||||||
|
defer tilemap.deinit();
|
||||||
|
|
||||||
|
var tilesets: Tiled.Tileset.List = .empty;
|
||||||
|
defer tilesets.deinit(plt.gpa);
|
||||||
|
|
||||||
|
try tilesets.add(plt.gpa, "tileset.tsx", try Tiled.Tileset.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/tileset.tsx")));
|
||||||
|
|
||||||
|
const bounds = tilemap.getTileBounds();
|
||||||
|
|
||||||
|
self.tilemap = try Tilemap.init(plt.gpa, bounds.width(), bounds.height());
|
||||||
|
|
||||||
|
const ground = tilemap.getLayer("Ground").?;
|
||||||
|
const walls = tilemap.getLayer("Walls").?;
|
||||||
|
for (0..bounds.height()) |oy| {
|
||||||
|
for (0..bounds.width()) |ox| {
|
||||||
|
const x = bounds.min_x + ox;
|
||||||
|
const y = bounds.min_y + oy;
|
||||||
|
|
||||||
|
const tile = self.tilemap.get(ox, oy);
|
||||||
|
|
||||||
|
if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| {
|
||||||
|
const pos = tiled_tile.getPosition();
|
||||||
|
tile.ground_sprite = tilesheet.get(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tilemap.getTile(walls, tilesets, x, y)) |tiled_tile| {
|
||||||
|
const pos = tiled_tile.getPosition();
|
||||||
|
tile.wall_sprite = tilesheet.get(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const markers = &tilemap.getLayer("Markers").?.variant.object;
|
||||||
|
const spawn = markers.getByName("spawn").?;
|
||||||
|
player_pos_x = @divFloor(@as(i32, @intFromFloat(spawn.shape.point.x)), @as(i32, @intCast(tilemap.tile_width)));
|
||||||
|
player_pos_x -= @intCast(bounds.min_x);
|
||||||
|
player_pos_y = @divFloor(@as(i32, @intFromFloat(spawn.shape.point.y)),@as(i32, @intCast(tilemap.tile_height)));
|
||||||
|
player_pos_y -= @intCast(bounds.min_y);
|
||||||
|
|
||||||
|
const boxes = tilemap.getLayer("Boxes").?;
|
||||||
|
for (0..bounds.height()) |oy| {
|
||||||
|
for (0..bounds.width()) |ox| {
|
||||||
|
const x = bounds.min_x + ox;
|
||||||
|
const y = bounds.min_y + oy;
|
||||||
|
|
||||||
|
if (tilemap.getTile(boxes, tilesets, x, y)) |tiled_tile| {
|
||||||
|
const pos = tiled_tile.getPosition();
|
||||||
|
_ = try self.spawnBox(@intCast(ox), @intCast(oy), tilesheet.get(pos.x, pos.y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const player_id = try self.spawnPlayer();
|
||||||
|
const player = self.entities.getAssumeExists(player_id);
|
||||||
|
player.pos_x = player_pos_x;
|
||||||
|
player.pos_y = player_pos_y;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sinSampleCallback(sound: *Audio.Sound) f32 {
|
fn spawnPlayer(self: *App) !Entity.Id {
|
||||||
return @sin(440 * sound.phase);
|
return try self.entities.insert(Entity{
|
||||||
|
.type = .player,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawnBox(self: *App, x: i32, y: i32, sprite: Gfx.Sprite.Id) !Entity.Id {
|
||||||
|
return try self.entities.insert(Entity{
|
||||||
|
.type = .box,
|
||||||
|
.pos_x = x,
|
||||||
|
.pos_y = y,
|
||||||
|
.sprite = sprite
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn frame(self: *App, plt: Platform.Frame) !void {
|
pub fn frame(self: *App, plt: Platform.Frame) !void {
|
||||||
const input = plt.input;
|
const input = plt.input;
|
||||||
|
|
||||||
const dt = plt.deltaTime();
|
var dir_y: i32 = 0;
|
||||||
|
var dir_x: i32 = 0;
|
||||||
var dir: Vec2 = .init(0, 0);
|
if (input.isKeyPressedOrRepeat(.S)) {
|
||||||
if (input.isKeyDown(.S)) {
|
dir_y += 1;
|
||||||
dir.y += 1;
|
|
||||||
}
|
}
|
||||||
if (input.isKeyDown(.W)) {
|
if (input.isKeyPressedOrRepeat(.W)) {
|
||||||
dir.y -= 1;
|
dir_y -= 1;
|
||||||
}
|
}
|
||||||
if (input.isKeyDown(.D)) {
|
if (input.isKeyPressedOrRepeat(.D)) {
|
||||||
dir.x += 1;
|
dir_x += 1;
|
||||||
}
|
}
|
||||||
if (input.isKeyDown(.A)) {
|
if (input.isKeyPressedOrRepeat(.A)) {
|
||||||
dir.x -= 1;
|
dir_x -= 1;
|
||||||
}
|
}
|
||||||
dir = dir.normalized();
|
if (dir_x != 0) {
|
||||||
self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt));
|
dir_y = 0;
|
||||||
|
|
||||||
if (input.isKeyPressed(.E)) {
|
|
||||||
Audio.setPlaying(self.sound, !Audio.getPlaying(self.sound));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var new_direction = self.player_direction;
|
Gfx.setClearColor(.rgb(40, 40, 40));
|
||||||
if (dir.x < 0) {
|
|
||||||
|
const tile_size = Vec2.init(48, 48);
|
||||||
|
for (0..self.tilemap.height) |y| {
|
||||||
|
for (0..self.tilemap.width) |x| {
|
||||||
|
const tile = self.tilemap.get(x, y);
|
||||||
|
|
||||||
|
if (tile.ground_sprite) |sprite| {
|
||||||
|
Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tile.wall_sprite) |sprite| {
|
||||||
|
Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var entity_iter = self.entities.iterator();
|
||||||
|
while (entity_iter.next()) |entity_id| {
|
||||||
|
const entity = self.entities.getAssumeExists(entity_id);
|
||||||
|
if (entity.type == .player) {
|
||||||
|
entity.pos_x += dir_x;
|
||||||
|
entity.pos_y += dir_y;
|
||||||
|
|
||||||
|
var new_direction = entity.player_direction;
|
||||||
|
if (dir_x < 0) {
|
||||||
new_direction = .left;
|
new_direction = .left;
|
||||||
} else if (dir.x > 0) {
|
} else if (dir_x > 0) {
|
||||||
new_direction = .right;
|
new_direction = .right;
|
||||||
} else if (dir.y > 0) {
|
} else if (dir_y > 0) {
|
||||||
new_direction = .down;
|
new_direction = .down;
|
||||||
} else if (dir.y < 0) {
|
} else if (dir_y < 0) {
|
||||||
new_direction = .up;
|
new_direction = .up;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.player_direction != new_direction) {
|
if (entity.player_direction != new_direction) {
|
||||||
self.player_frame_index = 0;
|
entity.player_frame_index = 0;
|
||||||
self.player_direction = new_direction;
|
entity.player_direction = new_direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const frames = self.player_sprites.getDirection(self.player_direction);
|
const frames = self.player_sprites.getDirection(entity.player_direction);
|
||||||
|
|
||||||
if (dir.x != 0 or dir.y != 0) {
|
if (dir_x != 0 or dir_y != 0) {
|
||||||
self.animation_timer += plt.dt;
|
self.animation_timer += plt.input.delta_time;
|
||||||
const animatin_time = std.time.ns_per_s / 5;
|
const animatin_time = std.time.ns_per_s / 5;
|
||||||
while (self.animation_timer >= animatin_time) {
|
while (self.animation_timer >= animatin_time) {
|
||||||
self.player_frame_index = (self.player_frame_index + 1) % @as(u32, @intCast(frames.len));
|
entity.player_frame_index = (entity.player_frame_index + 1) % @as(u32, @intCast(frames.len));
|
||||||
self.animation_timer -= animatin_time;
|
self.animation_timer -= animatin_time;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.animation_timer = 0;
|
self.animation_timer = 0;
|
||||||
self.player_frame_index = 0;
|
entity.player_frame_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir_x != 0 or dir_y != 0) {
|
||||||
|
self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .bus = self.sfx_bus });
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.sprite = frames[entity.player_frame_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (entity.sprite) |sprite| {
|
||||||
|
Gfx.drawSprite(
|
||||||
|
sprite,
|
||||||
|
Vec2.initFromInt(i32, entity.pos_x, entity.pos_y).multiply(tile_size),
|
||||||
|
tile_size,
|
||||||
|
.white
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Gfx.setClearColor(.rgb(40, 40, 40));
|
|
||||||
Gfx.drawSprite(frames[self.player_frame_index], self.player_pos, .init(64, 64), .white);
|
|
||||||
Gfx.drawSprite(self.tilesheet.get(11, 6), self.player_pos.add(.init(0, 200)), .init(64, 64), .white);
|
|
||||||
Gfx.drawText(self.roboto_font, .{
|
|
||||||
.pos = .init(300, 100),
|
|
||||||
.text = "Hello, World!",
|
|
||||||
.height = 64,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
||||||
_ = plt; // autofix
|
self.tilemap.deinit(plt.gpa);
|
||||||
_ = self; // autofix
|
self.entities.slots.deinit(plt.gpa);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,47 +1,66 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = std.log.scoped(.audio);
|
const log = std.log.scoped(.audio);
|
||||||
const Io = std.Io;
|
const Io = std.Io;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
const build_options = @import("build_options");
|
||||||
|
|
||||||
const sokol = @import("sokol");
|
const sokol = @import("sokol");
|
||||||
const sapp = sokol.app;
|
const sapp = sokol.app;
|
||||||
const saudio = sokol.audio;
|
const saudio = sokol.audio;
|
||||||
|
|
||||||
|
const ImGUI = @import("imgui.zig");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
|
const STBVorbis = @import("stb_vorbis");
|
||||||
|
const Math = @import("math");
|
||||||
|
|
||||||
const SlotMapType = @import("./slot_map.zig").SlotMapType;
|
const SlotMapType = @import("./slot_map.zig").SlotMapType;
|
||||||
|
|
||||||
const State = struct {
|
const Bus = struct {
|
||||||
io: Io,
|
label: ?[]const u8 = null,
|
||||||
mutex: std.Io.Mutex,
|
volume: f32 = 1,
|
||||||
sounds: Sound.SlotMap,
|
parent_id: ?Bus.Id = null,
|
||||||
volume: f32 = 0.1,
|
|
||||||
running: std.atomic.Value(bool) = .init(false),
|
|
||||||
|
|
||||||
temp_buffer: []f32
|
pub const Id = packed struct {
|
||||||
|
index: u16,
|
||||||
|
|
||||||
|
pub const nil = Id{ .index = std.math.maxInt(u16) };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
pub const BusId = Bus.Id;
|
||||||
|
|
||||||
|
const Buffer = struct {
|
||||||
|
samples: Samples,
|
||||||
|
|
||||||
|
const Samples = struct {
|
||||||
|
len: usize,
|
||||||
|
left: [*]const f32,
|
||||||
|
|
||||||
|
// If `right` is is null, then this is mono
|
||||||
|
right: ?[*]const f32,
|
||||||
|
|
||||||
|
pub fn deinit(self: Samples, gpa: Allocator) void {
|
||||||
|
gpa.free(self.left[0..self.len]);
|
||||||
|
if (self.right) |right| {
|
||||||
|
gpa.free(right[0..self.len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var g_state: State = undefined;
|
pub const Id = packed struct {
|
||||||
|
index: u16,
|
||||||
|
|
||||||
pub const InitOptions = struct {
|
pub const nil = Id{ .index = std.math.maxInt(u16) };
|
||||||
logger: saudio.Logger = .{},
|
|
||||||
buffer_frames: u32 = 512,
|
|
||||||
max_sounds: usize = 128,
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
pub const BufferId = Buffer.Id;
|
||||||
|
|
||||||
pub const Sound = struct {
|
const Sound = struct {
|
||||||
cb: Callback,
|
cursor: usize,
|
||||||
userdata: ?*anyopaque,
|
buffer: BufferId,
|
||||||
volume: f32,
|
volume: f32 = 1,
|
||||||
looping: bool,
|
loop: bool = false,
|
||||||
adsr: ADSR,
|
bus: BusId,
|
||||||
playing: bool,
|
|
||||||
|
|
||||||
phase_increment: f32 = 0,
|
|
||||||
duration_frames: u32 = 0,
|
|
||||||
|
|
||||||
frame_index: u32 = 0,
|
|
||||||
phase: f32 = 0,
|
|
||||||
|
|
||||||
pub const ADSR = struct {
|
pub const ADSR = struct {
|
||||||
attack: f32,
|
attack: f32,
|
||||||
@ -49,7 +68,7 @@ pub const Sound = struct {
|
|||||||
sustain: f32,
|
sustain: f32,
|
||||||
release: f32,
|
release: f32,
|
||||||
|
|
||||||
pub const default = ADSR{
|
pub const identity = ADSR{
|
||||||
.attack = 0,
|
.attack = 0,
|
||||||
.decay = 0,
|
.decay = 0,
|
||||||
.sustain = 1,
|
.sustain = 1,
|
||||||
@ -57,195 +76,544 @@ pub const Sound = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Callback = union(enum) {
|
const SlotMap = SlotMapType(u16, u8, Sound);
|
||||||
sample: *const fn(sound: *Sound) f32,
|
const Id = SlotMap.Id;
|
||||||
block: *const fn(sound: *Sound, samples: *std.ArrayList(f32)) void,
|
};
|
||||||
|
pub const SoundId = Sound.Id;
|
||||||
|
|
||||||
|
const ThreadState = struct {
|
||||||
|
// When modifying any fields in this struct, you must first acquire the mutex.
|
||||||
|
mutex: std.Io.Mutex,
|
||||||
|
|
||||||
|
sounds: Sound.SlotMap,
|
||||||
|
bus_volumes: []f32,
|
||||||
|
|
||||||
|
elapsed_time_ms: ?[]f32 = null,
|
||||||
|
budget_reached_counter: u32 = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const SlotMap = SlotMapType(u8, u16, Sound);
|
const State = struct {
|
||||||
pub const Id = SlotMap.Id;
|
io: Io,
|
||||||
|
gpa: Allocator,
|
||||||
|
running: std.atomic.Value(bool) = .init(false),
|
||||||
|
|
||||||
|
vorbis_alloc_buffer: []u8,
|
||||||
|
|
||||||
|
buses: std.ArrayList(Bus),
|
||||||
|
buffers: std.ArrayList(Buffer),
|
||||||
|
|
||||||
|
thread_state: ThreadState
|
||||||
|
};
|
||||||
|
|
||||||
|
var g_state: State = undefined;
|
||||||
|
pub var main_bus: BusId = .nil;
|
||||||
|
const g_sample_rate = 44100;
|
||||||
|
|
||||||
|
pub const InitOptions = struct {
|
||||||
|
logger: saudio.Logger = .{},
|
||||||
|
buffer_frames: u32 = 512,
|
||||||
|
|
||||||
|
max_buffers: usize = 128,
|
||||||
|
max_sounds: usize = 256,
|
||||||
|
max_buses: usize = 16,
|
||||||
|
max_vorbis_alloc_buffer_size: u32 = 1 * Math.bytes_per_mib,
|
||||||
|
|
||||||
|
track_elapsed_time: bool = build_options.has_imgui
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(io: Io, gpa: std.mem.Allocator, opts: InitOptions) !void {
|
pub fn init(io: Io, gpa: std.mem.Allocator, opts: InitOptions) !void {
|
||||||
const channels = 1; // TODO: Stereo audio
|
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
|
|
||||||
const sounds_buffer = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds);
|
var buses = try std.ArrayList(Bus).initCapacity(gpa, opts.max_buses);
|
||||||
errdefer gpa.free(sounds_buffer);
|
errdefer buses.deinit(gpa);
|
||||||
|
|
||||||
const temp_buffer = try gpa.alloc(f32, opts.buffer_frames);
|
const bus_volumes = try gpa.alloc(f32, opts.max_buses);
|
||||||
errdefer gpa.free(temp_buffer);
|
errdefer gpa.free(bus_volumes);
|
||||||
|
|
||||||
|
var buffers = try std.ArrayList(Buffer).initCapacity(gpa, opts.max_buffers);
|
||||||
|
errdefer buffers.deinit(gpa);
|
||||||
|
|
||||||
|
const sounds = try gpa.alloc(Sound.SlotMap.Slot, opts.max_sounds);
|
||||||
|
errdefer gpa.free(sounds);
|
||||||
|
|
||||||
|
const vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size);
|
||||||
|
errdefer gpa.free(vorbis_alloc_buffer);
|
||||||
|
|
||||||
self.* = State{
|
self.* = State{
|
||||||
.io = io,
|
.io = io,
|
||||||
|
.gpa = gpa,
|
||||||
|
.buffers = buffers,
|
||||||
|
.buses = buses,
|
||||||
|
.vorbis_alloc_buffer = vorbis_alloc_buffer,
|
||||||
|
.thread_state = .{
|
||||||
.mutex = .init,
|
.mutex = .init,
|
||||||
.sounds = .init(sounds_buffer),
|
.sounds = .initBuffer(sounds),
|
||||||
.temp_buffer = temp_buffer,
|
.bus_volumes = bus_volumes,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
self.running.store(true, .seq_cst);
|
|
||||||
|
|
||||||
|
main_bus = addBus(.{
|
||||||
|
.label = "main"
|
||||||
|
});
|
||||||
|
|
||||||
|
const channels = 2;
|
||||||
saudio.setup(.{
|
saudio.setup(.{
|
||||||
.stream_cb = sokolStreamCallback,
|
.stream_cb = sokolStreamCallback,
|
||||||
.logger = opts.logger,
|
.logger = opts.logger,
|
||||||
.num_channels = channels,
|
.num_channels = channels,
|
||||||
.buffer_frames = @intCast(opts.buffer_frames),
|
.buffer_frames = @intCast(opts.buffer_frames),
|
||||||
|
.sample_rate = g_sample_rate
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug("Init:", .{});
|
if (opts.track_elapsed_time) {
|
||||||
log.debug("- sample_rate: {}", .{saudio.sampleRate()});
|
const measurements_per_second: usize = @intCast(@divFloor(std.time.ns_per_s, getMaxAudioThreadDuration().nanoseconds));
|
||||||
log.debug("- channels: {}", .{saudio.channels()});
|
self.thread_state.elapsed_time_ms = try gpa.alloc(f32, 3*measurements_per_second);
|
||||||
log.debug("- buffer_frames: {}", .{saudio.bufferFrames()});
|
@memset(self.thread_state.elapsed_time_ms.?, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.running.store(true, .seq_cst);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(gpa: std.mem.Allocator) void {
|
pub fn deinit(gpa: std.mem.Allocator) void {
|
||||||
var self = &g_state;
|
var self = &g_state;
|
||||||
|
|
||||||
|
{
|
||||||
|
self.thread_state.mutex.lock(self.io) catch @panic("Failed to lock audio mutex");
|
||||||
|
defer self.thread_state.mutex.unlock(self.io);
|
||||||
|
|
||||||
self.running.store(false, .seq_cst);
|
self.running.store(false, .seq_cst);
|
||||||
|
}
|
||||||
|
|
||||||
saudio.shutdown();
|
saudio.shutdown();
|
||||||
|
|
||||||
gpa.free(self.sounds.slots.allocatedSlice());
|
for (self.buffers.items) |buffer| {
|
||||||
gpa.free(self.temp_buffer);
|
buffer.samples.deinit(gpa);
|
||||||
|
}
|
||||||
|
self.buffers.deinit(gpa);
|
||||||
|
|
||||||
|
self.buses.deinit(gpa);
|
||||||
|
gpa.free(self.thread_state.sounds.slots.allocatedSlice());
|
||||||
|
gpa.free(self.thread_state.bus_volumes);
|
||||||
|
if (self.thread_state.elapsed_time_ms) |elapsed_time_ms| {
|
||||||
|
gpa.free(elapsed_time_ms);
|
||||||
|
}
|
||||||
|
gpa.free(self.vorbis_alloc_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSampleRate() f32 {
|
const BufferData = union(enum) {
|
||||||
return @floatFromInt(saudio.sampleRate());
|
raw: struct {
|
||||||
}
|
samples_mono: []f32,
|
||||||
|
sample_rate: u32
|
||||||
pub const SoundOptions = struct {
|
},
|
||||||
cb: Sound.Callback,
|
vorbis: []const u8
|
||||||
userdata: ?*anyopaque = null,
|
|
||||||
adsr: Sound.ADSR = .default,
|
|
||||||
volume: f32 = 1,
|
|
||||||
playing: bool = true,
|
|
||||||
looping: bool = false,
|
|
||||||
|
|
||||||
frequency: ?f32 = null,
|
|
||||||
duration_ns: ?u64 = null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn initSound(opts: SoundOptions) Sound.Id {
|
// Source: https://amini-allight.org/post/game-audio-programming-tutorial-part-6
|
||||||
var self = &g_state;
|
fn hermiteResample(input: []f32, output: []f32) void {
|
||||||
|
for (0..output.len) |i| {
|
||||||
|
const elapsed = @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(output.len));
|
||||||
|
|
||||||
self.mutex.lock(self.io) catch return .nil;
|
const b_index: isize = @intFromFloat(elapsed * @as(f32, @floatFromInt(input.len)));
|
||||||
defer self.mutex.unlock(self.io);
|
const a_index = b_index - 1;
|
||||||
|
const c_index = b_index + 1;
|
||||||
|
const d_index = b_index + 2;
|
||||||
|
|
||||||
const id = self.sounds.insertUndefined() catch {
|
const a = if (a_index >= 0 and a_index < input.len) input[@intCast(a_index)] else input[@intCast(b_index)];
|
||||||
log.warn("Sound limit reached! limit: {}", .{self.sounds.slots.capacity});
|
const b = input[@intCast(b_index)];
|
||||||
|
const c = if (c_index >= 0 and c_index < input.len) input[@intCast(c_index)] else input[@intCast(b_index)];
|
||||||
|
const d = if (d_index >= 0 and d_index < input.len) input[@intCast(d_index)] else input[@intCast(b_index)];
|
||||||
|
|
||||||
|
const t = (elapsed * @as(f32, @floatFromInt(input.len))) - @as(f32, @floatFromInt(b_index));
|
||||||
|
|
||||||
|
const c0 = b;
|
||||||
|
const c1 = (c - a) * 0.5;
|
||||||
|
const c2 = a - (b * 2.5) + (c * 2.0) - (d * 0.5);
|
||||||
|
const c3 = ((b - c) * 1.5) + ((d - a) * 0.5);
|
||||||
|
output[i] = (((((c3 * t) + c2) * t) + c1) * t) + c0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resampleIfNeeded(gpa: Allocator, samples: *[]f32, sample_rate: u32) !void {
|
||||||
|
const new_sample_count = samples.len * g_sample_rate / sample_rate;
|
||||||
|
if (new_sample_count != samples.len) {
|
||||||
|
const new_samples = try gpa.alloc(f32, new_sample_count);
|
||||||
|
hermiteResample(samples.*, new_samples);
|
||||||
|
gpa.free(samples.*);
|
||||||
|
samples.* = new_samples;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bufferDataToSamples(data: BufferData) !Buffer.Samples {
|
||||||
|
const self = &g_state;
|
||||||
|
return switch (data) {
|
||||||
|
.raw => |raw| {
|
||||||
|
var samples = try self.gpa.dupe(f32, raw.samples_mono);
|
||||||
|
errdefer self.gpa.free(samples);
|
||||||
|
|
||||||
|
try resampleIfNeeded(self.gpa, &samples, raw.sample_rate);
|
||||||
|
|
||||||
|
return Buffer.Samples{
|
||||||
|
.len = samples.len,
|
||||||
|
.left = samples.ptr,
|
||||||
|
.right = null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
.vorbis => |vorbis_data| {
|
||||||
|
const vorbis = try STBVorbis.init(vorbis_data, self.vorbis_alloc_buffer);
|
||||||
|
const sample_count = vorbis.getStreamLengthInSamples();
|
||||||
|
|
||||||
|
const info = vorbis.getInfo();
|
||||||
|
if (info.channels == 1) {
|
||||||
|
var samples = try self.gpa.alloc(f32, sample_count);
|
||||||
|
errdefer self.gpa.free(samples);
|
||||||
|
|
||||||
|
const result = vorbis.getSamples(&.{ samples.ptr }, sample_count);
|
||||||
|
assert(result == sample_count);
|
||||||
|
|
||||||
|
try resampleIfNeeded(self.gpa, &samples, info.sample_rate);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.len = samples.len,
|
||||||
|
.left = samples.ptr,
|
||||||
|
.right = null
|
||||||
|
};
|
||||||
|
} else if (info.channels >= 2) {
|
||||||
|
var left_samples = try self.gpa.alloc(f32, sample_count);
|
||||||
|
errdefer self.gpa.free(left_samples);
|
||||||
|
|
||||||
|
var right_samples = try self.gpa.alloc(f32, sample_count);
|
||||||
|
errdefer self.gpa.free(right_samples);
|
||||||
|
|
||||||
|
const result = vorbis.getSamples(&.{ left_samples.ptr, right_samples.ptr }, sample_count);
|
||||||
|
assert(result == sample_count);
|
||||||
|
|
||||||
|
try resampleIfNeeded(self.gpa, &left_samples, info.sample_rate);
|
||||||
|
try resampleIfNeeded(self.gpa, &right_samples, info.sample_rate);
|
||||||
|
|
||||||
|
return Buffer.Samples{
|
||||||
|
.len = left_samples.len,
|
||||||
|
.left = left_samples.ptr,
|
||||||
|
.right = right_samples.ptr
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return Buffer.Samples{
|
||||||
|
.len = 0,
|
||||||
|
.left = &.{},
|
||||||
|
.right = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addBuffer(data: BufferData) BufferId {
|
||||||
|
const self = &g_state;
|
||||||
|
|
||||||
|
if (self.buffers.items.len == self.buffers.capacity) {
|
||||||
|
log.warn("Max audio buffers reached, limit: {}", .{self.buffers.capacity});
|
||||||
|
return .nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
const samples = bufferDataToSamples(data) catch |e| {
|
||||||
|
log.err("Failed to add buffer: {}", .{e});
|
||||||
return .nil;
|
return .nil;
|
||||||
};
|
};
|
||||||
|
|
||||||
var phase_increment: f32 = 0;
|
const buffer_id = BufferId{ .index = @intCast(self.buffers.items.len) };
|
||||||
if (opts.frequency) |frequency| {
|
const buffer = self.buffers.addOneAssumeCapacity();
|
||||||
phase_increment = frequency * 2 * std.math.pi / getSampleRate();
|
buffer.* = Buffer{
|
||||||
}
|
.samples = samples
|
||||||
|
|
||||||
var duration_frames: u32 = 0;
|
|
||||||
if (opts.duration_ns) |duration_ns| {
|
|
||||||
duration_frames = @intCast(duration_ns * @as(u64, @intCast(saudio.sampleRate())) / std.time.ns_per_s);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sound = self.sounds.getAssumeExists(id);
|
|
||||||
sound.* = Sound{
|
|
||||||
.cb = opts.cb,
|
|
||||||
.userdata = opts.userdata,
|
|
||||||
.adsr = opts.adsr,
|
|
||||||
.looping = opts.looping,
|
|
||||||
.volume = opts.volume,
|
|
||||||
.playing = opts.playing,
|
|
||||||
.duration_frames = duration_frames,
|
|
||||||
.phase_increment = phase_increment
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return id;
|
assert(buffer_id != BufferId.nil);
|
||||||
|
|
||||||
|
return buffer_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinitSound(id: Sound.Id) void {
|
pub fn addBus(opts: Bus) BusId {
|
||||||
var self = &g_state;
|
|
||||||
|
|
||||||
if (self.sounds.get(id)) |sound| {
|
|
||||||
self.mutex.lock(self.io) catch return;
|
|
||||||
defer self.mutex.unlock(self.io);
|
|
||||||
|
|
||||||
_ = sound; // autofix
|
|
||||||
self.sounds.removeAssumeExists(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setPlaying(id: Sound.Id, playing: bool) void {
|
|
||||||
var self = &g_state;
|
|
||||||
|
|
||||||
if (self.sounds.get(id)) |sound| {
|
|
||||||
self.mutex.lock(self.io) catch return;
|
|
||||||
defer self.mutex.unlock(self.io);
|
|
||||||
|
|
||||||
sound.playing = playing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getPlaying(id: Sound.Id) bool {
|
|
||||||
var self = &g_state;
|
|
||||||
|
|
||||||
if (self.sounds.get(id)) |sound| {
|
|
||||||
return sound.playing;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bumpSoundCursor(sound: *Sound, count: u32) void {
|
|
||||||
if (sound.duration_frames > 0) {
|
|
||||||
sound.frame_index += count;
|
|
||||||
sound.frame_index = @mod(sound.frame_index, sound.duration_frames);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sound.phase_increment > 0) {
|
|
||||||
sound.phase += sound.phase_increment * @as(f32, @floatFromInt(count));
|
|
||||||
sound.phase = @mod(sound.phase, 2 * std.math.pi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolStream(buffer: [*c]f32, num_frames: u32, num_channels: u32) !void {
|
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
|
|
||||||
|
if (self.buses.items.len == self.buses.capacity) {
|
||||||
|
log.warn("Max audio buses reached, limit: {}", .{self.buses.capacity});
|
||||||
|
return .nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus_id = BusId{ .index = @intCast(self.buses.items.len) };
|
||||||
|
const bus = self.buses.addOneAssumeCapacity();
|
||||||
|
bus.* = opts;
|
||||||
|
|
||||||
|
if (bus.parent_id != null and bus.parent_id == BusId.nil) {
|
||||||
|
bus.parent_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bus.parent_id == null and main_bus != BusId.nil) {
|
||||||
|
bus.parent_id = main_bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(bus_id != BusId.nil);
|
||||||
|
|
||||||
|
return bus_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn busMultipliedVolume(id: BusId) f32 {
|
||||||
|
const self = &g_state;
|
||||||
|
|
||||||
|
var result: f32 = 1;
|
||||||
|
var iter: ?BusId = id;
|
||||||
|
while (iter) |current| {
|
||||||
|
const bus = &self.buses.items[current.index];
|
||||||
|
result *= bus.volume;
|
||||||
|
iter = bus.parent_id;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlayOptions = struct {
|
||||||
|
buffer: BufferId,
|
||||||
|
// TODO: adsr: ?Sound.ADSR = null,
|
||||||
|
volume: f32 = 1,
|
||||||
|
loop: bool = false,
|
||||||
|
bus: ?BusId = null
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn play(opts: PlayOptions) SoundId {
|
||||||
|
const self = &g_state;
|
||||||
|
|
||||||
|
var mutex = &self.thread_state.mutex;
|
||||||
|
var sounds = &self.thread_state.sounds;
|
||||||
|
|
||||||
|
mutex.lock(self.io) catch |e| {
|
||||||
|
log.warn("Failed to lock mutex: {}", .{e});
|
||||||
|
return .nil;
|
||||||
|
};
|
||||||
|
defer mutex.unlock(self.io);
|
||||||
|
|
||||||
|
const bus = opts.bus orelse main_bus;
|
||||||
|
if (bus == BusId.nil) {
|
||||||
|
return .nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.buffer == BufferId.nil) {
|
||||||
|
return .nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sounds.unusedCapacity() == 0) {
|
||||||
|
log.warn("Max sounds reached, limit: {}", .{sounds.slots.capacity});
|
||||||
|
return .nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sound_id = sounds.insertAssumeCapacity();
|
||||||
|
const sound = sounds.getAssumeExists(sound_id);
|
||||||
|
sound.* = Sound{
|
||||||
|
.cursor = 0,
|
||||||
|
.volume = opts.volume,
|
||||||
|
.loop = opts.loop,
|
||||||
|
.buffer = opts.buffer,
|
||||||
|
.bus = bus,
|
||||||
|
};
|
||||||
|
|
||||||
|
return sound_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(id: SoundId) void {
|
||||||
|
const self = &g_state;
|
||||||
|
|
||||||
|
var mutex = &self.thread_state.mutex;
|
||||||
|
var sounds = &self.thread_state.sounds;
|
||||||
|
|
||||||
|
mutex.lock(self.io) catch |e| {
|
||||||
|
log.warn("Failed to lock mutex: {}", .{e});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer mutex.unlock(self.io);
|
||||||
|
|
||||||
|
_ = sounds.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getBus(id: BusId) ?*Bus {
|
||||||
|
const self = &g_state;
|
||||||
|
|
||||||
|
if (id == .nil) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return &self.buses.items[id.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn showDebug() void {
|
||||||
|
const self = &g_state;
|
||||||
|
|
||||||
|
const sounds = &self.thread_state.sounds;
|
||||||
|
const buses = &self.buses;
|
||||||
|
const buffers = &self.buffers;
|
||||||
|
|
||||||
|
_ = ImGUI.beginTabBar("audio tab bar");
|
||||||
|
defer ImGUI.endTabBar();
|
||||||
|
|
||||||
|
if (ImGUI.beginTabItem("General")) {
|
||||||
|
defer ImGUI.endTabItem();
|
||||||
|
|
||||||
|
ImGUI.text("Sample rate: {}Hz", .{saudio.sampleRate()});
|
||||||
|
ImGUI.text("Buffer frames: {}", .{saudio.bufferFrames()});
|
||||||
|
ImGUI.text("Max callback duration: {f}", .{getMaxAudioThreadDuration()});
|
||||||
|
|
||||||
|
ImGUI.separator();
|
||||||
|
ImGUI.text("Buffers: {}/{}", .{buffers.items.len, buffers.capacity});
|
||||||
|
ImGUI.text("Buses: {}/{}", .{buses.items.len, buses.capacity});
|
||||||
|
ImGUI.text("Sounds: {}/{}", .{sounds.count(), sounds.slots.capacity});
|
||||||
|
|
||||||
|
if (self.thread_state.elapsed_time_ms) |elapsed_time_ms| {
|
||||||
|
ImGUI.separator();
|
||||||
|
|
||||||
|
ImGUI.plotLines(.{
|
||||||
|
.label = "Elapsed (ms)",
|
||||||
|
.values = elapsed_time_ms,
|
||||||
|
.ex = .{
|
||||||
|
.scale_max = getMaxAudioThreadDurationMs()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var min_elapsed = elapsed_time_ms[0];
|
||||||
|
var max_elapsed = elapsed_time_ms[0];
|
||||||
|
var sum_elapsed = elapsed_time_ms[0];
|
||||||
|
for (elapsed_time_ms[1..]) |duration_ms| {
|
||||||
|
min_elapsed = @min(min_elapsed, duration_ms);
|
||||||
|
max_elapsed = @max(max_elapsed, duration_ms);
|
||||||
|
sum_elapsed += duration_ms;
|
||||||
|
}
|
||||||
|
const avg_elapsed = sum_elapsed / @as(f32, @floatFromInt(elapsed_time_ms.len));
|
||||||
|
ImGUI.text("Min/Max/Avg: {:.3}ms/{:.3}ms/{:.3}ms{s}", .{
|
||||||
|
min_elapsed, max_elapsed, avg_elapsed,
|
||||||
|
if (max_elapsed > getMaxAudioThreadDurationMs()) " !!!" else ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGUI.separator();
|
||||||
|
ImGUI.text("Budget reached counter: {}", .{self.thread_state.budget_reached_counter});
|
||||||
|
}
|
||||||
|
if (ImGUI.beginTabItem("Buses")) {
|
||||||
|
defer ImGUI.endTabItem();
|
||||||
|
if(ImGUI.beginTable("buses", 3, 0)) {
|
||||||
|
defer ImGUI.endTable();
|
||||||
|
|
||||||
|
ImGUI.tableSetupColumn("ID (Label)", 0);
|
||||||
|
ImGUI.tableSetupColumn("Volume", 0);
|
||||||
|
ImGUI.tableSetupColumn("Multiplied volume", 0);
|
||||||
|
ImGUI.tableHeadersRow();
|
||||||
|
|
||||||
|
for (0.., buses.items) |i, *bus| {
|
||||||
|
ImGUI.pushID(.{ .int = @intCast(i) });
|
||||||
|
defer ImGUI.popID();
|
||||||
|
|
||||||
|
ImGUI.tableNextRow();
|
||||||
|
|
||||||
|
ImGUI.tableNextColumn();
|
||||||
|
ImGUI.text("{} ({s})", .{i, bus.label orelse "-"});
|
||||||
|
|
||||||
|
ImGUI.tableNextColumn();
|
||||||
|
_ = ImGUI.inputF32Drag(.{
|
||||||
|
.label = "",
|
||||||
|
.value = &bus.volume,
|
||||||
|
.ex = .{
|
||||||
|
.min = 0,
|
||||||
|
.max = 1,
|
||||||
|
.speed = 0.01
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ImGUI.tableNextColumn();
|
||||||
|
ImGUI.text("{}", .{busMultipliedVolume(.{ .index = @intCast(i) })});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getMaxAudioThreadDuration() Io.Duration {
|
||||||
|
const margin = 0.05;
|
||||||
|
return Io.Duration{
|
||||||
|
.nanoseconds = @divFloor((@as(i64, @intFromFloat(@as(f64, @floatFromInt(std.time.ns_per_s)) * (1 - margin)))) * @as(i64, saudio.bufferFrames()), saudio.sampleRate())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getMaxAudioThreadDurationMs() f32 {
|
||||||
|
return @floatCast(@as(f64, @floatFromInt(getMaxAudioThreadDuration().nanoseconds)) / std.time.ns_per_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sokolStream(output_buffer: [*c]f32, num_frames: u32, num_channels: u32) !void {
|
||||||
|
const self = &g_state;
|
||||||
|
|
||||||
|
const started_at = Io.Clock.awake.now(self.io);
|
||||||
|
|
||||||
|
var zone = tracy.initZone(@src(), .{});
|
||||||
|
defer zone.deinit();
|
||||||
|
|
||||||
|
const mutex = &self.thread_state.mutex;
|
||||||
|
const sounds = &self.thread_state.sounds;
|
||||||
|
const buses = self.buses.items;
|
||||||
|
const bus_volumes = self.thread_state.bus_volumes;
|
||||||
|
|
||||||
|
try mutex.lock(self.io);
|
||||||
|
defer mutex.unlock(self.io);
|
||||||
|
|
||||||
if (!self.running.load(.seq_cst)) {
|
if (!self.running.load(.seq_cst)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.mutex.lock(self.io);
|
assert(num_channels == 2);
|
||||||
defer self.mutex.unlock(self.io);
|
@memset(output_buffer[0..(num_frames*2)], 0);
|
||||||
|
|
||||||
assert(num_channels == 1);
|
for (0..buses.len) |i| {
|
||||||
assert(self.temp_buffer.len >= num_frames);
|
bus_volumes[i] = busMultipliedVolume(.{ .index = @intCast(i) });
|
||||||
|
|
||||||
@memset(buffer[0..num_frames], 0);
|
|
||||||
|
|
||||||
var iter = self.sounds.iterator();
|
|
||||||
while (iter.next()) |id| {
|
|
||||||
const sound = self.sounds.getAssumeExists(id);
|
|
||||||
if (!sound.playing) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var samples = std.ArrayList(f32).initBuffer(self.temp_buffer[0..num_frames]);
|
var sound_iter = sounds.iterator();
|
||||||
switch (sound.cb) {
|
while (sound_iter.next()) |sound_id| {
|
||||||
.block => |cb| {
|
const sound = sounds.getAssumeExists(sound_id);
|
||||||
cb(sound, &samples);
|
const buffer = &self.buffers.items[sound.buffer.index];
|
||||||
bumpSoundCursor(sound, @intCast(samples.items.len));
|
|
||||||
},
|
const bus_id = sound.bus;
|
||||||
.sample => |cb| {
|
const bus = buses[bus_id.index];
|
||||||
samples.expandToCapacity();
|
_ = bus; // autofix
|
||||||
for (samples.items) |*sample| {
|
|
||||||
sample.* = cb(sound);
|
const volume = sound.volume * bus_volumes[bus_id.index];
|
||||||
bumpSoundCursor(sound, 1);
|
|
||||||
|
const left_channel = buffer.samples.left;
|
||||||
|
const right_channel = buffer.samples.right orelse buffer.samples.left;
|
||||||
|
|
||||||
|
var output_index: usize = 0;
|
||||||
|
while (output_index < num_frames) {
|
||||||
|
while (sound.cursor < buffer.samples.len and output_index < num_frames) {
|
||||||
|
const left = left_channel[sound.cursor];
|
||||||
|
const right = right_channel[sound.cursor];
|
||||||
|
sound.cursor += 1;
|
||||||
|
|
||||||
|
output_buffer[2*output_index+0] += left * volume;
|
||||||
|
output_buffer[2*output_index+1] += right * volume;
|
||||||
|
output_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sound.loop) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (sound.cursor == buffer.samples.len) {
|
||||||
|
sound.cursor = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (0..samples.items.len) |i| {
|
if (!sound.loop and sound.cursor == buffer.samples.len) {
|
||||||
buffer[i] += samples.items[i] * sound.volume * self.volume;
|
sounds.removeAssumeExists(sound_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duration = started_at.durationTo(Io.Clock.awake.now(self.io));
|
||||||
|
if (self.thread_state.elapsed_time_ms) |elapsed_time_ms| {
|
||||||
|
@memmove(elapsed_time_ms[1..elapsed_time_ms.len], elapsed_time_ms[0..(elapsed_time_ms.len-1)]);
|
||||||
|
elapsed_time_ms[0] = @floatCast(@as(f64, @floatFromInt(duration.nanoseconds)) / std.time.ns_per_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration.nanoseconds > getMaxAudioThreadDuration().nanoseconds) {
|
||||||
|
self.thread_state.budget_reached_counter += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sokolStreamCallback(buffer: [*c]f32, num_frames_i32: i32, num_channels: i32) callconv(.c) void {
|
fn sokolStreamCallback(buffer: [*c]f32, num_frames_i32: i32, num_channels: i32) callconv(.c) void {
|
||||||
|
|||||||
@ -288,7 +288,7 @@ fn nextEvents(self: *FileWatcher) !?*File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(self: *FileWatcher, io: Io) !?[]const u8 {
|
pub fn next(self: *FileWatcher, io: Io) !?[]const u8 {
|
||||||
const now = Io.Clock.real.now(io);
|
const now = Io.Clock.awake.now(io);
|
||||||
|
|
||||||
var queue_overflow = false;
|
var queue_overflow = false;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@ -168,16 +168,16 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
|
|||||||
.transforms = .initBuffer(&self.transforms_buffer),
|
.transforms = .initBuffer(&self.transforms_buffer),
|
||||||
|
|
||||||
.textures_buffer = undefined,
|
.textures_buffer = undefined,
|
||||||
.textures = .init(&self.textures_buffer),
|
.textures = .initBuffer(&self.textures_buffer),
|
||||||
|
|
||||||
.sprites_buffer = undefined,
|
.sprites_buffer = undefined,
|
||||||
.sprites = .init(&self.sprites_buffer),
|
.sprites = .initBuffer(&self.sprites_buffer),
|
||||||
|
|
||||||
.spritesheets_buffer = undefined,
|
.spritesheets_buffer = undefined,
|
||||||
.spritesheets = .init(&self.spritesheets_buffer),
|
.spritesheets = .initBuffer(&self.spritesheets_buffer),
|
||||||
|
|
||||||
.fonts_buffer = undefined,
|
.fonts_buffer = undefined,
|
||||||
.fonts = .init(&self.fonts_buffer),
|
.fonts = .initBuffer(&self.fonts_buffer),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.default_spritesheet = initSpritesheet(.{});
|
self.default_spritesheet = initSpritesheet(.{});
|
||||||
@ -233,9 +233,6 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
repackSpritesheetIfNeeded(self.default_spritesheet);
|
|
||||||
rebuildSpritesheetTextureIfNeeded(self.default_spritesheet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit() void {
|
pub fn deinit() void {
|
||||||
@ -347,45 +344,75 @@ pub fn beginFrame() void {
|
|||||||
.swapchain = sglue.swapchain()
|
.swapchain = sglue.swapchain()
|
||||||
});
|
});
|
||||||
|
|
||||||
const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet);
|
self.bindings.views[shd.VIEW_tex].id = sg.invalid_id;
|
||||||
const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture);
|
|
||||||
self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view;
|
|
||||||
self.bindings.samplers[shd.SMP_smp] = self.linear_sampler;
|
self.bindings.samplers[shd.SMP_smp] = self.linear_sampler;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn showDebug() void {
|
pub fn showDebug() void {
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
_ = self; // autofix
|
|
||||||
|
|
||||||
// TODO: Improve this by using tables or somekind of filtering
|
_ = ImGUI.beginTabBar("graphics tab bar");
|
||||||
|
defer ImGUI.endTabBar();
|
||||||
|
|
||||||
// if (ImGUI.beginWindow(.{
|
if (ImGUI.beginTabItem("Spritesheets")) {
|
||||||
// .name = "graphics",
|
defer ImGUI.endTabItem();
|
||||||
// .size = .init(200, 200)
|
|
||||||
// })) {
|
if(ImGUI.beginTable("spritesheets", 2, 0)) {
|
||||||
// defer ImGUI.endWindow();
|
defer ImGUI.endTable();
|
||||||
//
|
|
||||||
// {
|
ImGUI.tableSetupColumn("ID", 0);
|
||||||
// ImGUI.text("Spritesheets:", .{});
|
ImGUI.tableSetupColumn("Size", 0);
|
||||||
// var spritesheet_iter = self.spritesheets.iterator();
|
ImGUI.tableHeadersRow();
|
||||||
// while (spritesheet_iter.next()) |spritesheet_id| {
|
|
||||||
// const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id);
|
var spritesheet_iter = self.spritesheets.iterator();
|
||||||
// const texture = self.textures.get(spritesheet.texture);
|
while (spritesheet_iter.next()) |spritesheet_id| {
|
||||||
// ImGUI.text("{f}:", .{spritesheet_id});
|
const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id);
|
||||||
// ImGUI.text(" - needs_texture_rebuild:{}", .{spritesheet.needs_texture_rebuild});
|
|
||||||
// ImGUI.text(" - texture:{?}", .{texture});
|
ImGUI.pushID(.{ .int = spritesheet_id.index });
|
||||||
// }
|
defer ImGUI.popID();
|
||||||
// }
|
|
||||||
//
|
ImGUI.tableNextRow();
|
||||||
// {
|
|
||||||
// ImGUI.text("Sprites:", .{});
|
ImGUI.tableNextColumn();
|
||||||
// var sprite_iter = self.sprites.iterator();
|
ImGUI.text("{{ {}, {} }}", .{spritesheet_id.index, spritesheet_id.generation});
|
||||||
// while (sprite_iter.next()) |sprite_id| {
|
|
||||||
// const sprite = self.sprites.getAssumeExists(sprite_id);
|
ImGUI.tableNextColumn();
|
||||||
// ImGUI.text("{f} - {?}", .{sprite_id, sprite.position});
|
ImGUI.text("{}x{}", .{spritesheet.size.x, spritesheet.size.x});
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
if (ImGUI.beginTabItem("Sprites")) {
|
||||||
|
defer ImGUI.endTabItem();
|
||||||
|
|
||||||
|
if(ImGUI.beginTable("sprites", 2, 0)) {
|
||||||
|
defer ImGUI.endTable();
|
||||||
|
|
||||||
|
ImGUI.tableSetupColumn("ID", 0);
|
||||||
|
ImGUI.tableSetupColumn("Position", 0);
|
||||||
|
ImGUI.tableHeadersRow();
|
||||||
|
|
||||||
|
var sprite_iter = self.sprites.iterator();
|
||||||
|
while (sprite_iter.next()) |sprite_id| {
|
||||||
|
const sprite = self.sprites.getAssumeExists(sprite_id);
|
||||||
|
|
||||||
|
ImGUI.pushID(.{ .int = sprite_id.index });
|
||||||
|
defer ImGUI.popID();
|
||||||
|
|
||||||
|
ImGUI.tableNextRow();
|
||||||
|
|
||||||
|
ImGUI.tableNextColumn();
|
||||||
|
ImGUI.text("{{ {}, {} }}", .{sprite_id.index, sprite_id.generation});
|
||||||
|
|
||||||
|
ImGUI.tableNextColumn();
|
||||||
|
if (sprite.position) |position| {
|
||||||
|
ImGUI.text("{:3}, {:3}", .{position.x, position.y});
|
||||||
|
} else {
|
||||||
|
ImGUI.text("null", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This will always have a 1-frame delay.
|
// TODO: This will always have a 1-frame delay.
|
||||||
@ -396,11 +423,7 @@ pub fn setClearColor(color: Color) void {
|
|||||||
self.clear_color = color;
|
self.clear_color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlushOptions = struct {
|
pub fn flush() void {
|
||||||
rebuild_spritesheets: bool = true
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn flush(opts: FlushOptions) void {
|
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
|
|
||||||
if (self.quads.items.len == 0) {
|
if (self.quads.items.len == 0) {
|
||||||
@ -408,7 +431,6 @@ pub fn flush(opts: FlushOptions) void {
|
|||||||
}
|
}
|
||||||
defer self.quads.clearRetainingCapacity();
|
defer self.quads.clearRetainingCapacity();
|
||||||
|
|
||||||
if (opts.rebuild_spritesheets) {
|
|
||||||
var spritesheet_iter = self.spritesheets.iterator();
|
var spritesheet_iter = self.spritesheets.iterator();
|
||||||
while (spritesheet_iter.next()) |spritesheet_id| {
|
while (spritesheet_iter.next()) |spritesheet_id| {
|
||||||
const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id);
|
const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id);
|
||||||
@ -417,6 +439,10 @@ pub fn flush(opts: FlushOptions) void {
|
|||||||
rebuildSpritesheetTextureIfNeeded(spritesheet_id);
|
rebuildSpritesheetTextureIfNeeded(spritesheet_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sg.queryViewState(getBoundView().*) != .VALID) {
|
||||||
|
log.warn("Attempt to use view which isn't valid", .{});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vertex_buffer = self.bindings.vertex_buffers[0];
|
const vertex_buffer = self.bindings.vertex_buffers[0];
|
||||||
@ -442,7 +468,7 @@ pub fn flush(opts: FlushOptions) void {
|
|||||||
var fs_params: shd.FsParams = .{
|
var fs_params: shd.FsParams = .{
|
||||||
.texture_mode = 0,
|
.texture_mode = 0,
|
||||||
};
|
};
|
||||||
const image = sg.queryViewImage(self.bindings.views[shd.VIEW_tex]);
|
const image = sg.queryViewImage(getBoundView().*);
|
||||||
if (sg.queryImagePixelformat(image) == .R8) {
|
if (sg.queryImagePixelformat(image) == .R8) {
|
||||||
fs_params.texture_mode = 1;
|
fs_params.texture_mode = 1;
|
||||||
}
|
}
|
||||||
@ -451,17 +477,17 @@ pub fn flush(opts: FlushOptions) void {
|
|||||||
sg.applyBindings(self.bindings);
|
sg.applyBindings(self.bindings);
|
||||||
sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params));
|
sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params));
|
||||||
sg.applyUniforms(shd.UB_fs_params, sg.asRange(&fs_params));
|
sg.applyUniforms(shd.UB_fs_params, sg.asRange(&fs_params));
|
||||||
|
assert(self.quads.items.len > 0);
|
||||||
sg.draw(0, @intCast(self.quads.items.len * 6), 1);
|
sg.draw(0, @intCast(self.quads.items.len * 6), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn endFrame() void {
|
pub fn endFrame() void {
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
|
|
||||||
flush(.{});
|
flush();
|
||||||
|
|
||||||
sg.endPass();
|
sg.endPass();
|
||||||
sg.commit();
|
sg.commit();
|
||||||
self.frame_index += 1;
|
|
||||||
|
|
||||||
var spritesheet_iter = self.spritesheets.iterator();
|
var spritesheet_iter = self.spritesheets.iterator();
|
||||||
while (spritesheet_iter.next()) |spritesheet_id| {
|
while (spritesheet_iter.next()) |spritesheet_id| {
|
||||||
@ -473,6 +499,8 @@ pub fn endFrame() void {
|
|||||||
} else if (self.transforms.items.len == 0) {
|
} else if (self.transforms.items.len == 0) {
|
||||||
log.warn("Too many calls to transformPop()", .{});
|
log.warn("Too many calls to transformPop()", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.frame_index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void {
|
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void {
|
||||||
@ -577,33 +605,28 @@ pub fn initTexture(opts: TextureOptions) Texture.Id {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn getBoundView() *sg.View {
|
||||||
|
const self = &g_state;
|
||||||
|
return &self.bindings.views[shd.VIEW_tex];
|
||||||
|
}
|
||||||
|
|
||||||
fn isViewBound(view: sg.View) bool {
|
fn isViewBound(view: sg.View) bool {
|
||||||
const self = &g_state;
|
return getBoundView().id == view.id;
|
||||||
const bound_view = &self.bindings.views[shd.VIEW_tex];
|
|
||||||
return bound_view.id == view.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinitTextureResources(texture: *Texture) void {
|
|
||||||
const self = &g_state;
|
|
||||||
|
|
||||||
if (isViewBound(texture.view)) {
|
|
||||||
const default_sprite = self.sprites.getAssumeExists(self.default_sprite);
|
|
||||||
const default_spritesheet = self.spritesheets.getAssumeExists(default_sprite.spritesheet);
|
|
||||||
const default_texture = self.textures.getAssumeExists(default_spritesheet.texture);
|
|
||||||
bindView(default_texture.view, .{ .rebuild_spritesheets = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
sg.destroyImage(texture.image);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinitTexture(id: Texture.Id) void {
|
pub fn deinitTexture(id: Texture.Id) void {
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
if (id == self.nil_texture) {
|
if (id == .nil) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.textures.get(id)) |texture| {
|
if (self.textures.get(id)) |texture| {
|
||||||
deinitTextureResources(texture);
|
const bound_view = getBoundView();
|
||||||
|
if (bound_view.id == texture.view.id) {
|
||||||
|
bound_view.id = sg.invalid_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
sg.destroyImage(texture.image);
|
||||||
sg.destroyView(texture.view);
|
sg.destroyView(texture.view);
|
||||||
self.textures.removeAssumeExists(id);
|
self.textures.removeAssumeExists(id);
|
||||||
}
|
}
|
||||||
@ -649,7 +672,6 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void {
|
|||||||
|
|
||||||
var texture_updated: bool = false;
|
var texture_updated: bool = false;
|
||||||
if (sg.queryImageState(texture.image) == .VALID) {
|
if (sg.queryImageState(texture.image) == .VALID) {
|
||||||
// sg.queryImageInfo
|
|
||||||
const image_desc = sg.queryImageDesc(texture.image);
|
const image_desc = sg.queryImageDesc(texture.image);
|
||||||
if (image_desc.width == new_image_desc.width and
|
if (image_desc.width == new_image_desc.width and
|
||||||
image_desc.height == new_image_desc.height and
|
image_desc.height == new_image_desc.height and
|
||||||
@ -659,19 +681,19 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void {
|
|||||||
image_desc.usage.immutable == new_image_desc.usage.immutable and
|
image_desc.usage.immutable == new_image_desc.usage.immutable and
|
||||||
image_desc.usage.dynamic_update
|
image_desc.usage.dynamic_update
|
||||||
) {
|
) {
|
||||||
log.debug("Update texture '{f}'", .{id});
|
log.debug("[{}] Update texture '{f}'", .{self.frame_index, id});
|
||||||
if (texture.updated_at == null or texture.updated_at.? != self.frame_index) {
|
if (texture.updated_at == null or texture.updated_at.? != self.frame_index) {
|
||||||
sg.updateImage(texture.image, image_data);
|
sg.updateImage(texture.image, image_data);
|
||||||
texture_updated = true;
|
texture_updated = true;
|
||||||
texture.updated_at = self.frame_index;
|
texture.updated_at = self.frame_index;
|
||||||
} else {
|
} else {
|
||||||
log.warn("Attempt to update the same texture multiple times per frame", .{});
|
log.warn("Attempt to update the same texture multiple times per frame, texture: {f}", .{id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!texture_updated) {
|
if (!texture_updated) {
|
||||||
log.debug("Create image-view, texture={f}", .{id});
|
log.debug("[{}] Create image-view, texture={f}", .{self.frame_index, id});
|
||||||
const new_image = sg.makeImage(new_image_desc);
|
const new_image = sg.makeImage(new_image_desc);
|
||||||
if (sg.queryImageState(new_image) != .VALID) {
|
if (sg.queryImageState(new_image) != .VALID) {
|
||||||
log.warn("makeImage() failed", .{});
|
log.warn("makeImage() failed", .{});
|
||||||
@ -689,11 +711,13 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deinitTextureResources(texture);
|
sg.destroyImage(texture.image);
|
||||||
texture.image = new_image;
|
texture.image = new_image;
|
||||||
texture.updated_at = null;
|
texture.updated_at = null;
|
||||||
|
|
||||||
|
log.debug("is dynamic {f}, {}", .{id, texture.dynamic});
|
||||||
if (texture.dynamic) {
|
if (texture.dynamic) {
|
||||||
|
log.debug("update texture {f}", .{id});
|
||||||
sg.updateImage(new_image, image_data);
|
sg.updateImage(new_image, image_data);
|
||||||
texture.updated_at = self.frame_index;
|
texture.updated_at = self.frame_index;
|
||||||
}
|
}
|
||||||
@ -712,20 +736,11 @@ pub fn drawTexture(id: Texture.Id, pos: Vec2, size: Vec2, color: Color) void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bindView(view: sg.View, opts: FlushOptions) void {
|
fn bindView(view: sg.View) void {
|
||||||
|
_ = view; // autofix
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
|
_ = self; // autofix
|
||||||
|
|
||||||
const view_state = sg.queryViewState(view);
|
|
||||||
if (view_state != .VALID and view_state != .ALLOC) {
|
|
||||||
log.warn("Attempt to bind non-valid view: state={}", .{view_state});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bound_view = &self.bindings.views[shd.VIEW_tex];
|
|
||||||
if (bound_view.id != view.id) {
|
|
||||||
flush(opts);
|
|
||||||
bound_view.* = view;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setDefaultSampler(sampler: Sampler) void {
|
pub fn setDefaultSampler(sampler: Sampler) void {
|
||||||
@ -773,7 +788,17 @@ pub fn draw(opts: DrawOptions) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindView(view, .{});
|
const view_state = sg.queryViewState(view);
|
||||||
|
if (view_state == .VALID or view_state == .ALLOC) {
|
||||||
|
const bound_view = getBoundView();
|
||||||
|
if (bound_view.id != view.id) {
|
||||||
|
flush();
|
||||||
|
bound_view.* = view;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Attempt to bind non-valid view: state={}", .{view_state});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sampler = switch (opts.sampler orelse self.default_sampler) {
|
const sampler = switch (opts.sampler orelse self.default_sampler) {
|
||||||
.linear => self.linear_sampler,
|
.linear => self.linear_sampler,
|
||||||
@ -781,12 +806,12 @@ pub fn draw(opts: DrawOptions) void {
|
|||||||
};
|
};
|
||||||
const bound_sampler = &self.bindings.samplers[shd.SMP_smp];
|
const bound_sampler = &self.bindings.samplers[shd.SMP_smp];
|
||||||
if (bound_sampler.id != sampler.id) {
|
if (bound_sampler.id != sampler.id) {
|
||||||
flush(.{});
|
flush();
|
||||||
bound_sampler.* = sampler;
|
bound_sampler.* = sampler;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.quads.items.len == self.quads.capacity) {
|
if (self.quads.items.len == self.quads.capacity) {
|
||||||
flush(.{});
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getTransformPtr()) |transform| {
|
if (getTransformPtr()) |transform| {
|
||||||
@ -843,6 +868,7 @@ const SetSpriteOptions = struct {
|
|||||||
|
|
||||||
pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void {
|
pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void {
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
|
|
||||||
const sprite = self.sprites.get(id) orelse {
|
const sprite = self.sprites.get(id) orelse {
|
||||||
log.warn("Attempt to set sprite that doesn't exist: {}", .{id});
|
log.warn("Attempt to set sprite that doesn't exist: {}", .{id});
|
||||||
return;
|
return;
|
||||||
@ -897,12 +923,14 @@ pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void {
|
|||||||
|
|
||||||
const new_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height);
|
const new_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height);
|
||||||
|
|
||||||
if (old_size != null and old_size.?.eql(new_size) and sprite.position != null) {
|
log.debug("set sprite on {f}", .{sprite.spritesheet});
|
||||||
spritesheet.needs_texture_rebuild = true;
|
|
||||||
} else {
|
if (old_size == null or !old_size.?.eql(new_size) or sprite.position == null) {
|
||||||
sprite.position = null;
|
sprite.position = null;
|
||||||
spritesheet.needs_repack = true;
|
spritesheet.needs_repack = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spritesheet.needs_texture_rebuild = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WARNING: This will change the UV coordinates of all sprites.
|
// WARNING: This will change the UV coordinates of all sprites.
|
||||||
@ -915,7 +943,7 @@ fn repackSpritesheet(id: Spritesheet.Id) void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
log.debug("Repack spritesheet: {f}", .{id});
|
log.debug("[{}] Repack spritesheet: {f}", .{self.frame_index, id});
|
||||||
|
|
||||||
// TODO: Add a smarter startegy for picking the initial texture size.
|
// TODO: Add a smarter startegy for picking the initial texture size.
|
||||||
// One idea is to calculate the sum area of all sprites and pick a atlas size based on that.
|
// One idea is to calculate the sum area of all sprites and pick a atlas size based on that.
|
||||||
@ -994,7 +1022,7 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Rebuild spritesheet texture: {f}", .{id});
|
log.debug("[{}] Rebuild spritesheet texture: {f}", .{self.frame_index, id});
|
||||||
|
|
||||||
const image_data = ImageData.init(
|
const image_data = ImageData.init(
|
||||||
self.gpa,
|
self.gpa,
|
||||||
@ -1265,12 +1293,12 @@ pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Fo
|
|||||||
};
|
};
|
||||||
glyph = font.cache.get(glyph_id);
|
glyph = font.cache.get(glyph_id);
|
||||||
glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y);
|
glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y);
|
||||||
|
glyph.sprite = null;
|
||||||
|
|
||||||
const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0);
|
const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0);
|
||||||
const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0);
|
const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0);
|
||||||
if (box_width > 0 and box_height > 0) {
|
if (box_width > 0 and box_height > 0) {
|
||||||
assert(glyph.sprite == Sprite.Id.nil);
|
const sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 });
|
||||||
glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 });
|
|
||||||
|
|
||||||
assert(box_width < Font.Glyph.max_width);
|
assert(box_width < Font.Glyph.max_width);
|
||||||
assert(box_height < Font.Glyph.max_height);
|
assert(box_height < Font.Glyph.max_height);
|
||||||
@ -1288,9 +1316,10 @@ pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Fo
|
|||||||
glyph_key.scale_x, glyph_key.scale_y,
|
glyph_key.scale_x, glyph_key.scale_y,
|
||||||
glyph_key.index
|
glyph_key.index
|
||||||
);
|
);
|
||||||
setSprite(glyph.sprite, .{ .image = bitmap });
|
setSprite(sprite, .{ .image = bitmap });
|
||||||
} else {
|
|
||||||
glyph.sprite = .nil;
|
assert(glyph.sprite == null);
|
||||||
|
glyph.sprite = sprite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
glyph.used_this_frame = true;
|
glyph.used_this_frame = true;
|
||||||
@ -1308,7 +1337,7 @@ pub const TextRunLayout = struct {
|
|||||||
|
|
||||||
const GlyphRect = struct {
|
const GlyphRect = struct {
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
sprite: Sprite.Id,
|
sprite: ?Sprite.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(font: Font.Id, height: f32) TextRunLayout {
|
pub fn init(font: Font.Id, height: f32) TextRunLayout {
|
||||||
@ -1406,9 +1435,10 @@ pub fn drawText(id: Font.Id, opts: DrawTextOptions) void {
|
|||||||
while (iter.nextCodepoint()) |codepoint| {
|
while (iter.nextCodepoint()) |codepoint| {
|
||||||
const glyph_index = getGlyphIndex(id, codepoint) orelse continue;
|
const glyph_index = getGlyphIndex(id, codepoint) orelse continue;
|
||||||
const glyph_layout = layout.next(glyph_index) orelse continue;
|
const glyph_layout = layout.next(glyph_index) orelse continue;
|
||||||
|
const glyph_sprite = glyph_layout.sprite orelse continue;
|
||||||
|
|
||||||
drawSprite(
|
drawSprite(
|
||||||
glyph_layout.sprite,
|
glyph_sprite,
|
||||||
glyph_layout.rect.pos.add(pos),
|
glyph_layout.rect.pos.add(pos),
|
||||||
glyph_layout.rect.size,
|
glyph_layout.rect.size,
|
||||||
opts.color
|
opts.color
|
||||||
@ -2133,7 +2163,7 @@ pub const Font = struct {
|
|||||||
key: Key,
|
key: Key,
|
||||||
|
|
||||||
box: STBTrueType.Box,
|
box: STBTrueType.Box,
|
||||||
sprite: Sprite.Id,
|
sprite: ?Sprite.Id,
|
||||||
used_this_frame: bool,
|
used_this_frame: bool,
|
||||||
|
|
||||||
const max_width: u32 = 512;
|
const max_width: u32 = 512;
|
||||||
@ -2169,7 +2199,9 @@ pub const Font = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: Glyph) void {
|
fn deinit(self: Glyph) void {
|
||||||
deinitSprite(self.sprite);
|
if (self.sprite) |sprite| {
|
||||||
|
deinitSprite(sprite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -503,3 +503,92 @@ pub fn collapsingHeader(label: []const u8) bool {
|
|||||||
ig.ImGuiTreeNodeFlags_None
|
ig.ImGuiTreeNodeFlags_None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn beginTable(id: [*c]const u8, columns: u32, flags: ig.ImGuiTableFlags) bool {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ig.igBeginTable(id, @intCast(columns), flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endTable() void {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ig.igEndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tableNextColumn() void {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ig.igTableNextColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tableNextRow() void {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ig.igTableNextRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tableSetColumnIndex(index: usize) void {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ig.igTableSetColumnIndex(@intCast(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tableSetupColumn(label: [*c]const u8, flags: ig.ImGuiTableColumnFlags) void {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ig.igTableSetupColumn(label, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tableHeadersRow() void {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ig.igTableHeadersRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlotLinesOptions = struct {
|
||||||
|
label: [*c]const u8,
|
||||||
|
values: []f32,
|
||||||
|
ex: ?struct {
|
||||||
|
graph_width: f32 = 0,
|
||||||
|
graph_height: f32 = 0,
|
||||||
|
scale_min: f32 = 0,
|
||||||
|
scale_max: f32 = 1,
|
||||||
|
stride: u32 = 4,
|
||||||
|
} = null
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn plotLines(opts: PlotLinesOptions) void {
|
||||||
|
if (isDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.ex) |ex| {
|
||||||
|
ig.igPlotLinesEx(
|
||||||
|
opts.label,
|
||||||
|
opts.values.ptr, @intCast(opts.values.len),
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
ex.scale_min,
|
||||||
|
ex.scale_max,
|
||||||
|
.{ .x = ex.graph_width, .y = ex.graph_height },
|
||||||
|
@intCast(ex.stride)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ig.igPlotLines(opts.label, opts.values.ptr, @intCast(opts.values.len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -6,9 +6,16 @@ const sokol = @import("sokol");
|
|||||||
const Math = @import("math");
|
const Math = @import("math");
|
||||||
const Vec2 = Math.Vec2;
|
const Vec2 = Math.Vec2;
|
||||||
|
|
||||||
|
const Nanoseconds = @import("./root.zig").Nanoseconds;
|
||||||
|
|
||||||
const Input = @This();
|
const Input = @This();
|
||||||
|
|
||||||
key_code_mapping: std.EnumMap(KeyCode, u21),
|
// TODO: Make this configurable from the app
|
||||||
|
const key_repeat_first: Nanoseconds = 0.4 * std.time.ns_per_s;
|
||||||
|
const key_repeat_delay: Nanoseconds = 0.2 * std.time.ns_per_s;
|
||||||
|
|
||||||
|
now: Nanoseconds,
|
||||||
|
delta_time: Nanoseconds,
|
||||||
|
|
||||||
window_size: Vec2,
|
window_size: Vec2,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
@ -18,8 +25,13 @@ mouse_position: ?Vec2,
|
|||||||
mouse_delta: Vec2,
|
mouse_delta: Vec2,
|
||||||
mouse_scroll: Vec2,
|
mouse_scroll: Vec2,
|
||||||
|
|
||||||
|
key_code_mapping: std.EnumMap(KeyCode, u21),
|
||||||
|
last_key_repeat_at: std.EnumMap(KeyCode, Nanoseconds),
|
||||||
|
|
||||||
pub fn init(window_size: Vec2) Input {
|
pub fn init(window_size: Vec2) Input {
|
||||||
return Input{
|
return Input{
|
||||||
|
.now = 0,
|
||||||
|
.delta_time = 0,
|
||||||
.key_code_mapping = .{},
|
.key_code_mapping = .{},
|
||||||
.focused = false,
|
.focused = false,
|
||||||
.keyboard = .empty,
|
.keyboard = .empty,
|
||||||
@ -27,7 +39,8 @@ pub fn init(window_size: Vec2) Input {
|
|||||||
.mouse_position = null,
|
.mouse_position = null,
|
||||||
.mouse_delta = .init(0, 0),
|
.mouse_delta = .init(0, 0),
|
||||||
.mouse_scroll = .init(0, 0),
|
.mouse_scroll = .init(0, 0),
|
||||||
.window_size = window_size
|
.window_size = window_size,
|
||||||
|
.last_key_repeat_at = .{}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +53,33 @@ pub fn isKeyReleased(self: Input, key: KeyCode) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn isKeyDown(self: Input, key: KeyCode) bool {
|
pub fn isKeyDown(self: Input, key: KeyCode) bool {
|
||||||
return self.keyboard.down.contains(key);
|
return self.keyboard.pressed_at.contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getKeyDown(self: Input, key: KeyCode) ?Nanoseconds {
|
||||||
|
return self.keyboard.getDown(key, self.now);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isKeyRepeat(self: *Input, key: KeyCode) bool {
|
||||||
|
const pressed_at = self.keyboard.pressed_at.get(key) orelse return false;
|
||||||
|
|
||||||
|
var next_repeat_at: Nanoseconds = undefined;
|
||||||
|
if (self.last_key_repeat_at.get(key)) |last_repeat_at| {
|
||||||
|
next_repeat_at = last_repeat_at + key_repeat_delay;
|
||||||
|
} else {
|
||||||
|
next_repeat_at = pressed_at + key_repeat_first;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.now >= next_repeat_at) {
|
||||||
|
self.last_key_repeat_at.put(key, next_repeat_at);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isKeyPressedOrRepeat(self: *Input, key: KeyCode) bool {
|
||||||
|
return self.isKeyPressed(key) or self.isKeyRepeat(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isMousePressed(self: Input, button: MouseButton) bool {
|
pub fn isMousePressed(self: Input, button: MouseButton) bool {
|
||||||
@ -52,7 +91,11 @@ pub fn isMouseReleased(self: Input, button: MouseButton) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn isMouseDown(self: Input, button: MouseButton) bool {
|
pub fn isMouseDown(self: Input, button: MouseButton) bool {
|
||||||
return self.mouse_buttons.down.contains(button);
|
return self.mouse_buttons.pressed_at.contains(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getMouseDown(self: Input, button: MouseButton) ?Nanoseconds {
|
||||||
|
return self.mouse_buttons.getDown(button, self.now);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) {
|
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) {
|
||||||
@ -212,28 +255,35 @@ fn KeyStateType(T: type) type {
|
|||||||
return struct {
|
return struct {
|
||||||
pressed: std.EnumSet(T),
|
pressed: std.EnumSet(T),
|
||||||
released: std.EnumSet(T),
|
released: std.EnumSet(T),
|
||||||
down: std.EnumSet(T),
|
pressed_at: std.EnumMap(T, Nanoseconds),
|
||||||
|
|
||||||
pub const empty = @This(){
|
pub const empty = @This(){
|
||||||
.pressed = .empty,
|
.pressed = .empty,
|
||||||
.released = .empty,
|
.released = .empty,
|
||||||
.down = .empty,
|
.pressed_at = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn press(self: *@This(), key: T) void {
|
pub fn press(self: *@This(), key: T, now: Nanoseconds) void {
|
||||||
self.pressed.insert(key);
|
self.pressed.insert(key);
|
||||||
self.down.insert(key);
|
self.pressed_at.put(key, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn release(self: *@This(), key: T) void {
|
pub fn release(self: *@This(), key: T) void {
|
||||||
self.released.insert(key);
|
self.released.insert(key);
|
||||||
self.down.remove(key);
|
self.pressed_at.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getDown(self: @This(), key: T, now: Nanoseconds) ?Nanoseconds {
|
||||||
|
if (self.pressed_at.get(key)) |pressed_at| {
|
||||||
|
return now - pressed_at;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn releaseAll(self: *@This()) void {
|
pub fn releaseAll(self: *@This()) void {
|
||||||
var iter = self.down.iterator();
|
var iter = self.pressed_at.iterator();
|
||||||
while (iter.next()) |key| {
|
while (iter.next()) |e| {
|
||||||
self.release(key);
|
self.release(e.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,6 +27,7 @@ pub const ImGUI = @import("./imgui.zig");
|
|||||||
pub const Gfx = @import("./graphics.zig");
|
pub const Gfx = @import("./graphics.zig");
|
||||||
pub const Input = @import("./input.zig");
|
pub const Input = @import("./input.zig");
|
||||||
pub const Audio = @import("./audio.zig");
|
pub const Audio = @import("./audio.zig");
|
||||||
|
pub const SlotMapType = @import("./slot_map.zig").SlotMapType;
|
||||||
|
|
||||||
const EmbeddedAssets = @import("embedded_assets");
|
const EmbeddedAssets = @import("embedded_assets");
|
||||||
|
|
||||||
@ -105,10 +106,11 @@ fn PlatformType(App: type) type {
|
|||||||
_ = self.frame_arena.reset(.free_all); // TODO: make arena reset mode configurable
|
_ = self.frame_arena.reset(.free_all); // TODO: make arena reset mode configurable
|
||||||
|
|
||||||
const now = Io.Timestamp.now(self.io, .awake);
|
const now = Io.Timestamp.now(self.io, .awake);
|
||||||
const t = self.started_at.durationTo(now);
|
|
||||||
const dt = self.last_frame_at.durationTo(now);
|
|
||||||
self.last_frame_at = now;
|
self.last_frame_at = now;
|
||||||
|
|
||||||
|
self.input.now = @intCast(self.started_at.durationTo(now).nanoseconds);
|
||||||
|
self.input.delta_time = @intCast(self.last_frame_at.durationTo(now).nanoseconds);
|
||||||
|
|
||||||
if (self.input.keyboard.pressed.contains(.F4)) {
|
if (self.input.keyboard.pressed.contains(.F4)) {
|
||||||
self.show_imgui = !self.show_imgui;
|
self.show_imgui = !self.show_imgui;
|
||||||
}
|
}
|
||||||
@ -126,8 +128,6 @@ fn PlatformType(App: type) type {
|
|||||||
|
|
||||||
if (@hasDecl(App, "frame")) {
|
if (@hasDecl(App, "frame")) {
|
||||||
const plt = Frame{
|
const plt = Frame{
|
||||||
.t = t.nanoseconds,
|
|
||||||
.dt = dt.nanoseconds,
|
|
||||||
.gpa = self.gpa,
|
.gpa = self.gpa,
|
||||||
.arena = self.arena.allocator(),
|
.arena = self.arena.allocator(),
|
||||||
.io = self.io,
|
.io = self.io,
|
||||||
@ -139,8 +139,26 @@ fn PlatformType(App: type) type {
|
|||||||
try self.app.frame(plt);
|
try self.app.frame(plt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Gfx.flush(.{});
|
Gfx.flush();
|
||||||
|
if (ImGUI.beginWindow(.{
|
||||||
|
.name = "Platform",
|
||||||
|
.size = .init(300, 400)
|
||||||
|
})) {
|
||||||
|
defer ImGUI.endWindow();
|
||||||
|
|
||||||
|
_ = ImGUI.beginTabBar("platform tab bar");
|
||||||
|
defer ImGUI.endTabBar();
|
||||||
|
|
||||||
|
if (ImGUI.beginTabItem("Graphics")) {
|
||||||
|
defer ImGUI.endTabItem();
|
||||||
Gfx.showDebug();
|
Gfx.showDebug();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGUI.beginTabItem("Audio")) {
|
||||||
|
defer ImGUI.endTabItem();
|
||||||
|
Audio.showDebug();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGUI.endFrame();
|
ImGUI.endFrame();
|
||||||
Gfx.endFrame();
|
Gfx.endFrame();
|
||||||
@ -177,7 +195,9 @@ fn PlatformType(App: type) type {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.keyboard.press(key);
|
const now = self.started_at.durationTo(Io.Timestamp.now(self.io, .awake));
|
||||||
|
|
||||||
|
input.keyboard.press(key, @intCast(now.nanoseconds));
|
||||||
self.last_key_pressed = key;
|
self.last_key_pressed = key;
|
||||||
},
|
},
|
||||||
.key_released => |key| {
|
.key_released => |key| {
|
||||||
@ -186,13 +206,16 @@ fn PlatformType(App: type) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input.keyboard.release(key);
|
input.keyboard.release(key);
|
||||||
|
input.last_key_repeat_at.remove(key);
|
||||||
},
|
},
|
||||||
.mouse_pressed => |button| {
|
.mouse_pressed => |button| {
|
||||||
if (input.mouse_position == null or input.isMouseDown(button)) {
|
if (input.mouse_position == null or input.isMouseDown(button)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.mouse_buttons.press(button);
|
const now = self.started_at.durationTo(Io.Timestamp.now(self.io, .awake));
|
||||||
|
|
||||||
|
input.mouse_buttons.press(button, @intCast(now.nanoseconds));
|
||||||
},
|
},
|
||||||
.mouse_released => |button| {
|
.mouse_released => |button| {
|
||||||
if (input.mouse_position == null or !input.isMouseDown(button)) {
|
if (input.mouse_position == null or !input.isMouseDown(button)) {
|
||||||
@ -256,8 +279,8 @@ fn PlatformType(App: type) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(input.mouse_position == null);
|
assert(input.mouse_position == null);
|
||||||
assert(input.keyboard.down.eql(.empty));
|
assert(input.keyboard.pressed_at.count() == 0);
|
||||||
assert(input.mouse_buttons.down.eql(.empty));
|
assert(input.mouse_buttons.pressed_at.count() == 0);
|
||||||
input.focused = false;
|
input.focused = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -288,13 +311,13 @@ fn PlatformType(App: type) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e == .unfocused) {
|
if (e == .unfocused) {
|
||||||
var key_iter = input.keyboard.down.iterator();
|
var key_iter = input.keyboard.pressed_at.iterator();
|
||||||
while (key_iter.next()) |key| {
|
while (key_iter.next()) |key_pressed_at| {
|
||||||
self.appendEvent(.{ .key_released = key });
|
self.appendEvent(.{ .key_released = key_pressed_at.key });
|
||||||
}
|
}
|
||||||
var mouse_buttons_iter = input.mouse_buttons.down.iterator();
|
var mouse_buttons_iter = input.mouse_buttons.pressed_at.iterator();
|
||||||
while (mouse_buttons_iter.next()) |button| {
|
while (mouse_buttons_iter.next()) |key_pressed_at| {
|
||||||
self.appendEvent(.{ .mouse_released = button });
|
self.appendEvent(.{ .mouse_released = key_pressed_at.key });
|
||||||
}
|
}
|
||||||
self.appendEvent(.{ .mouse_leave = {} });
|
self.appendEvent(.{ .mouse_leave = {} });
|
||||||
}
|
}
|
||||||
@ -548,12 +571,9 @@ pub const Deinit = struct {
|
|||||||
io: Io,
|
io: Io,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Nanoseconds = i96;
|
pub const Nanoseconds = i64;
|
||||||
|
|
||||||
pub const Frame = struct {
|
pub const Frame = struct {
|
||||||
t: Nanoseconds,
|
|
||||||
dt: Nanoseconds,
|
|
||||||
|
|
||||||
gpa: Allocator,
|
gpa: Allocator,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
frame: Allocator,
|
frame: Allocator,
|
||||||
@ -562,16 +582,16 @@ pub const Frame = struct {
|
|||||||
input: *Input,
|
input: *Input,
|
||||||
input_events: []Input.Event,
|
input_events: []Input.Event,
|
||||||
|
|
||||||
fn nanosecondsToSeconds(nanoseconds: i96) f32 {
|
fn nanosecondsToSeconds(nanoseconds: Nanoseconds) f32 {
|
||||||
return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s);
|
return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deltaTime(self: Frame) f32 {
|
pub fn deltaTime(self: Frame) f32 {
|
||||||
return nanosecondsToSeconds(self.dt);
|
return nanosecondsToSeconds(self.input.delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time(self: Frame) f32 {
|
pub fn time(self: Frame) f32 {
|
||||||
return nanosecondsToSeconds(self.t);
|
return nanosecondsToSeconds(self.input.now);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -606,7 +626,7 @@ pub fn run(App: type, opts: RunOptions) void {
|
|||||||
.last_key_press_is_repeat = false,
|
.last_key_press_is_repeat = false,
|
||||||
.last_key_pressed = null,
|
.last_key_pressed = null,
|
||||||
.frame_arena = .init(gpa),
|
.frame_arena = .init(gpa),
|
||||||
.show_imgui = builtin.mode == .Debug,
|
.show_imgui = false,
|
||||||
.assets = .init(gpa, io, assets_dir),
|
.assets = .init(gpa, io, assets_dir),
|
||||||
.last_mouse_position = null
|
.last_mouse_position = null
|
||||||
};
|
};
|
||||||
|
|||||||
@ -87,7 +87,7 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn clearRetainingCapacity(self: *Self) void {
|
pub fn clearRetainingCapacity(self: *Self) void {
|
||||||
self.* = .init(self.slots.items);
|
self.* = .initBuffer(self.slots.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insertHole(self: *Self, index: Index) void {
|
fn insertHole(self: *Self, index: Index) void {
|
||||||
@ -129,7 +129,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
|
|||||||
|
|
||||||
pub fn unusedCapacity(self: *Self) usize {
|
pub fn unusedCapacity(self: *Self) usize {
|
||||||
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
|
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
|
||||||
return capacity - self.slots.items.len + self.hole_count;
|
return capacity - self.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(self: *Self) usize {
|
||||||
|
return self.slots.items.len - self.hole_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insertAssumeCapacity(self: *Self) Id {
|
pub fn insertAssumeCapacity(self: *Self) Id {
|
||||||
@ -215,11 +219,21 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(slots: []Slot) Self {
|
pub fn initBuffer(slots: []Slot) Self {
|
||||||
var self: Self = .empty;
|
var self: Self = .empty;
|
||||||
self.slots = .initBuffer(slots);
|
self.slots = .initBuffer(slots);
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn initCapacity(gpa: Allocator, capacity: usize) !Self {
|
||||||
|
var self: Self = .empty;
|
||||||
|
self.slots = try .initCapacity(gpa, capacity);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self, gpa: Allocator) void {
|
||||||
|
self.slots.deinit(gpa);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user