From: hgn Date: Sat, 15 Jan 2022 20:31:24 +0000 (+0000) Subject: switched to command based rendering X-Git-Url: https://harrygodden.com/git/?p=fishladder.git;a=commitdiff_plain;h=3cb144985169da3cea1c21a7b0bfba8d43c28fdb switched to command based rendering --- diff --git a/build.sh b/build.sh index aa0d03e..63f64de 100755 --- a/build.sh +++ b/build.sh @@ -65,6 +65,7 @@ if [ "$compile_tools" = true ]; then mkdir tools -p gcc -Wall -Wstrict-aliasing=3 $lib $flags mdlcomp.c gl/glad.c -o tools/mdlcomp $libs $steam_part -Wl,-rpath=./ $defines gcc -Wall -Wstrict-aliasing=3 $lib $flags fontcomp.c gl/glad.c -o tools/fontcomp $libs $steam_part -Wl,-rpath=./ $defines + gcc -Wall -Wstrict-aliasing=3 $lib $flags texsheet.c gl/glad.c -o tools/texsheet $libs $steam_part -Wl,-rpath=./ $defines gcc $lib qoiconv.c -std=c99 -O3 -o tools/qoiconv fi @@ -77,13 +78,15 @@ for f in textures/*.png; ./tools/qoiconv $f ./_temp_textures/"$(basename "$f" .png).qoi" done -#if [ "$compile_models" = true ]; then -# echo "Recompiling models" -# for f in models/*.obj; -# do echo "Compiling $f.."; -# ./tools/mdlcomp $f $f.h -# done -#fi +# Autocombine textures +auto_combine="" +cd textures_combine +for f in *.png; + do echo "[combine] $f"; + auto_combine="$auto_combine $f" +done +../tools/texsheet ../_temp_textures/autocombine.qoi ../sprites_autocombine.h sprites_auto_combine $auto_combine +cd .. # Main build if [ "$do_build" = true ]; then diff --git a/fishladder.c b/fishladder.c index 74f9726..e0cfd67 100644 --- a/fishladder.c +++ b/fishladder.c @@ -218,6 +218,15 @@ static struct world } *data; #pragma pack(pop) + + struct render_cmd + { + struct cell *ptr; + v2i pos; + } + *cmd_buf_tiles, *cmd_buf_specials; + + u32 tile_count, tile_special_count, max_commands; int initialzed; int sim_run, max_runs; @@ -275,7 +284,7 @@ static struct world v2f physics_v; v2f physics_co; } - fishes[16]; + fishes[64]; int num_fishes; @@ -351,7 +360,8 @@ static int world_check_pos_ok( v2i co, int dist ); static int cell_interactive( v2i co ); void vg_update(void); -static void render_tiles( v2i start, v2i end, v4f const regular_colour, v4f const selected_colour ); +static void render_tiles( v4f const regular_colour, v4f const selected_colour ); +static void render_tile_block( v2i start, v2i end, v4f const regular_colour, v4f const selected_colour ); void vg_render(void); void vg_ui(void); @@ -510,6 +520,8 @@ static void map_free(void) { arrfree( world.data ); arrfree( world.io ); + + free( world.cmd_buf_tiles ); world.w = 0; world.h = 0; @@ -1112,6 +1124,10 @@ static int map_load( const char *str, const char *name ) btn->position[1] = world.h -i -2; } + // Allocate buffers for render commands + world.cmd_buf_tiles = malloc( world.w*world.h* sizeof( struct render_cmd ) ); + world.max_commands = world.w*world.h; + return 1; IL_REG_ERROR: @@ -1797,7 +1813,7 @@ static void vg_update(void) { cell->state &= ~FLAG_FLIP_ROTATING; } - if( cell->state & FLAG_IS_TRIGGER ) + if( cell->state & (FLAG_IS_TRIGGER|FLAG_EMITTER) ) cell->state &= ~FLAG_TRIGGERED; } @@ -1984,9 +2000,12 @@ static void vg_update(void) { struct fish *fish = &world.fishes[i]; + // Apply movement if( fish->state == k_fish_state_alive ) - { v2i_add( fish->pos, fish->dir, fish->pos ); + + if( fish->state == k_fish_state_alive || fish->state == k_fish_state_soon_alive ) + { struct cell *cell_current = pcell( fish->pos ); if( cell_current->state & FLAG_IS_TRIGGER ) @@ -1995,7 +2014,8 @@ static void vg_update(void) struct cell *target_peice = &world.data[ cell_current->links[trigger_id] ]; - if( target_peice->state & FLAG_EMITTER ) + // Spawn new marble + if( (target_peice->state & FLAG_EMITTER) && !(target_peice->state & FLAG_TRIGGERED)) { struct fish *fish = &world.fishes[ world.num_fishes ]; lcell( cell_current->links[trigger_id], fish->pos ); @@ -2317,7 +2337,42 @@ static void vg_update(void) } } -static void render_tiles( v2i start, v2i end, v4f const regular_colour, v4f const selected_colour ) +static void render_tile( v2i pos, struct cell *ptr, v4f const regular_colour, v4f const selected_colour ) +{ + int selected = world.selected == pos[1]*world.w + pos[0]; + + int tile_offsets[][2] = + { + {2, 0}, {0, 3}, {0, 2}, {2, 2}, + {1, 0}, {2, 3}, {3, 2}, {1, 3}, + {3, 1}, {0, 1}, {1, 2}, {2, 1}, + {1, 1}, {3, 3}, {2, 1}, {2, 1} + }; + + int uv[2]; + + uv[0] = tile_offsets[ ptr->config ][0]; + uv[1] = tile_offsets[ ptr->config ][1]; + + glUniform4f( SHADER_UNIFORM( shader_tile_main, "uOffset" ), + (float)pos[0], + (float)pos[1], + uv[0], + uv[1] + ); + + if( selected ) + { + glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, selected_colour ); + draw_mesh( 0, 2 ); + glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour ); + } + else + draw_mesh( 0, 2 ); +} + +// Renders specific chunk of tiles +static void render_tile_block( v2i start, v2i end, v4f const regular_colour, v4f const selected_colour ) { v2i full_start = { 0,0 }; v2i full_end = { world.w, world.h }; @@ -2327,41 +2382,44 @@ static void render_tiles( v2i start, v2i end, v4f const regular_colour, v4f cons start = full_start; end = full_end; } - - glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour ); for( int y = start[1]; y < end[1]; y ++ ) { for( int x = start[0]; x < end[0]; x ++ ) { - struct cell *cell = pcell((v2i){x,y}); - int selected = world.selected == y*world.w + x; - - int tile_offsets[][2] = - { - {2, 0}, {0, 3}, {0, 2}, {2, 2}, - {1, 0}, {2, 3}, {3, 2}, {1, 3}, - {3, 1}, {0, 1}, {1, 2}, {2, 1}, - {1, 1}, {3, 3}, {2, 1}, {2, 1} - }; - - int uv[2] = { 3, 0 }; - + v2i pos = { x, y }; + struct cell *cell = pcell( pos ); + if( cell->state & (FLAG_CANAL|FLAG_INPUT|FLAG_OUTPUT|FLAG_EMITTER) ) - { - uv[0] = tile_offsets[ cell->config ][0]; - uv[1] = tile_offsets[ cell->config ][1]; - } else continue; - - glUniform4f( SHADER_UNIFORM( shader_tile_main, "uOffset" ), (float)x, (float)y, uv[0], uv[1] ); - if( selected ) - { - glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, selected_colour ); - draw_mesh( 0, 2 ); - glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour ); - } - else - draw_mesh( 0, 2 ); + render_tile( pos, cell, regular_colour, selected_colour ); + } + } +} + +// Renders all tiles in the command list +static void render_tiles( v4f const regular_colour, v4f const selected_colour ) +{ + glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour ); + + struct render_list + { + struct render_cmd *arr; + u32 count; + } + render_lists[] = { + { world.cmd_buf_tiles, world.tile_count }, + { world.cmd_buf_specials, world.tile_special_count } + }; + + for( int i = 0; i < vg_list_size( render_lists ); i ++ ) + { + struct render_list *list = &render_lists[i]; + for( int j = 0; j < list->count; j ++ ) + { + struct render_cmd *cmd = &list->arr[j]; + struct cell *cell = cmd->ptr; + + render_tile( cmd->pos, cell, regular_colour, selected_colour ); } } } @@ -2501,6 +2559,34 @@ void vg_render(void) if( !world.initialzed ) return; + + // Extract render commands + world.tile_count = 0; + world.tile_special_count = 0; + + for( int y = 1; y < world.h-1; y ++ ) + { + for( int x = 1; x < world.w-1; x ++ ) + { + struct cell *cell = pcell((v2i){x,y}); + + if( cell->state & (FLAG_CANAL|FLAG_INPUT|FLAG_OUTPUT|FLAG_EMITTER) ) + { + struct render_cmd *cmd; + + if( cell->config == k_cell_type_split || (cell->state & FLAG_EMITTER ) ) + cmd = &world.cmd_buf_tiles[ world.max_commands - (++ world.tile_special_count) ]; + else + cmd = &world.cmd_buf_tiles[ world.tile_count ++ ]; + + cmd->pos[0] = x; + cmd->pos[1] = y; + cmd->ptr = cell; + } + } + } + + world.cmd_buf_specials = &world.cmd_buf_tiles[ world.max_commands - world.tile_special_count ]; // BACKGROUND // ======================================================================================================== @@ -2547,7 +2633,7 @@ void vg_render(void) vg_tex2d_bind( &tex_wood, 1 ); glUniform1i( SHADER_UNIFORM( shader_tile_main, "uTexWood" ), 1 ); - render_tiles( NULL, NULL, colour_default, colour_default ); + render_tiles( colour_default, colour_default ); // MARBLES // ======================================================================================================== @@ -2607,51 +2693,46 @@ void vg_render(void) glUniform1i( SHADER_UNIFORM( shader_tile_main, "uTexWood" ), 1 ); glUniform1f( SHADER_UNIFORM( shader_tile_main, "uForeground" ), 1.0f ); - render_tiles( NULL, NULL, colour_default, colour_selected ); + render_tiles( colour_default, colour_selected ); // Draw splitters - for( int y = 2; y < world.h-2; y ++ ) + for( int i = 0; i < world.tile_special_count; i ++ ) { - for( int x = 2; x < world.w-2; x ++ ) - { - struct cell *cell = pcell((v2i){x,y}); + struct render_cmd *cmd = &world.cmd_buf_specials[i]; + struct cell *cell = cmd->ptr; - if( cell->state & FLAG_CANAL ) + if( cell->config == k_cell_type_split ) + { + float rotation = cell->state & FLAG_FLIP_FLOP? vg_rad( -45.0f ): vg_rad( 45.0f ); + + if( cell->state & FLAG_FLIP_ROTATING ) { - if( cell->config == k_cell_type_split ) + if( (world.frame_lerp > curve_7_linear_section) ) { - float rotation = cell->state & FLAG_FLIP_FLOP? vg_rad( -45.0f ): vg_rad( 45.0f ); - - if( cell->state & FLAG_FLIP_ROTATING ) + float const rotation_speed = 0.4f; + if( (world.frame_lerp < 1.0f-rotation_speed) ) { - if( (world.frame_lerp > curve_7_linear_section) ) - { - float const rotation_speed = 0.4f; - if( (world.frame_lerp < 1.0f-rotation_speed) ) - { - float t = world.frame_lerp - curve_7_linear_section; - t *= -2.0f * (1.0f/(1.0f-(curve_7_linear_section+rotation_speed))); - t += 1.0f; - - rotation *= t; - } - else - rotation *= -1.0f; - } + float t = world.frame_lerp - curve_7_linear_section; + t *= -2.0f * (1.0f/(1.0f-(curve_7_linear_section+rotation_speed))); + t += 1.0f; + + rotation *= t; } - - m2x2_create_rotation( subtransform, rotation ); - - glUniformMatrix2fv( SHADER_UNIFORM( shader_tile_main, "uSubTransform" ), 1, GL_FALSE, (float *)subtransform ); - glUniform4f( SHADER_UNIFORM( shader_tile_main, "uOffset" ), - (float)x, - (float)y + 0.125f, - cell->state & FLAG_TARGETED? 3.0f: 0.0f, - 0.0f - ); - draw_mesh( 0, 2 ); + else + rotation *= -1.0f; } } + + m2x2_create_rotation( subtransform, rotation ); + + glUniformMatrix2fv( SHADER_UNIFORM( shader_tile_main, "uSubTransform" ), 1, GL_FALSE, (float *)subtransform ); + glUniform4f( SHADER_UNIFORM( shader_tile_main, "uOffset" ), + (float)cmd->pos[0], + (float)cmd->pos[1] + 0.125f, + cell->state & FLAG_TARGETED? 3.0f: 0.0f, + 0.0f + ); + draw_mesh( 0, 2 ); } } @@ -2670,7 +2751,7 @@ void vg_render(void) glUniformMatrix2fv( SHADER_UNIFORM( shader_tile_main, "uSubTransform" ), 1, GL_FALSE, (float *)subtransform ); glUniform2fv( SHADER_UNIFORM( shader_tile_main, "uMousePos" ), 1, world.tile_pos ); - render_tiles( new_begin, new_end, colour_default, colour_default ); + render_tile_block( new_begin, new_end, colour_default, colour_default ); world.data[ world.selected ].state ^= FLAG_CANAL; map_reclassify( new_begin, new_end, 0 ); @@ -2798,54 +2879,53 @@ void vg_render(void) float rp_xa = rp_x1*expf(1.0f-rp_x1)* 0.36f; float rp_x2 = 1.0f-rp_xa; - for( int y = 2; y < world.h-2; y ++ ) + for( int i = 0; i < world.tile_special_count; i ++ ) { - for( int x = 2; x < world.w-2; x ++ ) - { - struct cell *cell = pcell((v2i){x,y}); + struct render_cmd *cmd = &world.cmd_buf_specials[i]; + struct cell *cell = cmd->ptr; - if( cell->state & FLAG_CANAL ) + if( cell->state & FLAG_TARGETED ) + { + for( int j = 0; j < 2; j ++ ) { - if( cell->state & FLAG_IS_TRIGGER ) - { - int trigger_id = cell->links[0]?0:1; - struct cell *other_cell = &world.data[ cell->links[ trigger_id ]]; - - struct cell_description *desc = &cell_descriptions[ cell->config ]; - - int x2 = cell->links[trigger_id] % world.w; - int y2 = (cell->links[trigger_id] - x2) / world.w; - - v2f startpoint; - v2f endpoint; - - startpoint[0] = (float)x2 + (trigger_id? 0.75f: 0.25f); - startpoint[1] = (float)y2 + 0.25f; - - endpoint[0] = x; - endpoint[1] = y; - - v2_add( desc->trigger_pos, endpoint, endpoint ); - - if( other_cell->state & FLAG_EMITTER ) - { - v4f wire_colour; - colour_code_v3( other_cell->cc, wire_colour ); - wire_colour[3] = 1.0f; + if( !cell->links[j] ) + continue; - glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, wire_colour ); - } - else - { - glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, trigger_id? wire_right_colour: wire_left_colour ); - } + struct cell *other_cell = &world.data[ cell->links[ j ]]; + struct cell_description *desc = &cell_descriptions[ other_cell->config ]; + + int x2 = cell->links[j] % world.w; + int y2 = (cell->links[j] - x2) / world.w; + + v2f startpoint; + v2f endpoint; + + endpoint[0] = (float)cmd->pos[0] + (j? 0.75f: 0.25f); + endpoint[1] = (float)cmd->pos[1] + 0.25f; + + startpoint[0] = x2; + startpoint[1] = y2; + + v2_add( desc->trigger_pos, startpoint, startpoint ); + + if( cmd->ptr->state & FLAG_EMITTER ) + { + v4f wire_colour; + colour_code_v3( other_cell->cc, wire_colour ); + wire_colour[3] = 1.0f; - glUniform1f( SHADER_UNIFORM( shader_wire, "uCurve" ), cell->state & FLAG_TRIGGERED? rp_x2 * 0.4f: 0.4f ); - glUniform1f( SHADER_UNIFORM( shader_wire, "uGlow" ), cell->state & FLAG_TRIGGERED? rp_xa: 0.0f ); - glUniform3f( SHADER_UNIFORM( shader_wire, "uStart" ), startpoint[0], startpoint[1], 0.18f ); - glUniform3f( SHADER_UNIFORM( shader_wire, "uEnd" ), endpoint[0], endpoint[1], 0.18f ); - glDrawElements( GL_TRIANGLES, world.wire.em, GL_UNSIGNED_SHORT, (void*)(0) ); + glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, wire_colour ); } + else + { + glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, j? wire_right_colour: wire_left_colour ); + } + + glUniform1f( SHADER_UNIFORM( shader_wire, "uCurve" ), other_cell->state & FLAG_TRIGGERED? rp_x2 * 0.4f: 0.4f ); + glUniform1f( SHADER_UNIFORM( shader_wire, "uGlow" ), other_cell->state & FLAG_TRIGGERED? rp_xa: 0.0f ); + glUniform3f( SHADER_UNIFORM( shader_wire, "uEnd" ), startpoint[0], startpoint[1], 0.18f ); + glUniform3f( SHADER_UNIFORM( shader_wire, "uStart" ), endpoint[0], endpoint[1], 0.18f ); + glDrawElements( GL_TRIANGLES, world.wire.em, GL_UNSIGNED_SHORT, (void*)(0) ); } } } @@ -2857,81 +2937,78 @@ void vg_render(void) glUniformMatrix3fv( SHADER_UNIFORM( shader_tile_colour, "uPv" ), 1, GL_FALSE, (float *)vg_pv ); use_mesh( &world.shapes ); - for( int y = 2; y < world.h-2; y ++ ) + for( int i = 0; i < world.tile_special_count; i ++ ) { - for( int x = 2; x < world.w-2; x ++ ) - { - struct cell *cell = pcell((v2i){x,y}); + struct render_cmd *cmd = &world.cmd_buf_specials[i]; + struct cell *cell = cmd->ptr; - if( cell->state & FLAG_CANAL ) + if( cell->state & FLAG_TARGETED ) + { + for( int j = 0; j < 2; j ++ ) { - if( cell->state & FLAG_IS_TRIGGER ) - { - int trigger_id = cell->links[0]?0:1; - struct cell *other_cell = &world.data[ cell->links[ trigger_id ]]; - - struct cell_description *desc = &cell_descriptions[ cell->config ]; + if( !cell->links[j] ) + continue; + + struct cell *other_cell = &world.data[ cell->links[ j ]]; - int x2 = cell->links[trigger_id] % world.w; - int y2 = (cell->links[trigger_id] - x2) / world.w; - - v2f pts[2]; - - pts[0][0] = (float)x2 + (trigger_id? 0.75f: 0.25f); - pts[0][1] = (float)y2 + 0.25f; - - pts[1][0] = x; - pts[1][1] = y; - - v2_add( desc->trigger_pos, pts[1], pts[1] ); - - if( other_cell->state & FLAG_EMITTER ) - { - v4f wire_colour; - colour_code_v3( other_cell->cc, wire_colour ); + struct cell_description *desc = &cell_descriptions[ other_cell->config ]; + + int x2 = cell->links[j] % world.w; + int y2 = (cell->links[j] - x2) / world.w; + + v2f pts[2]; + + pts[0][0] = (float)cmd->pos[0] + (j? 0.75f: 0.25f); + pts[0][1] = (float)cmd->pos[1] + 0.25f; + + pts[1][0] = x2; + pts[1][1] = y2; + + v2_add( desc->trigger_pos, pts[1], pts[1] ); + + if( other_cell->state & FLAG_EMITTER ) + { + v4f wire_colour; + colour_code_v3( other_cell->cc, wire_colour ); - v3_muls( wire_colour, 0.8f, wire_colour ); - wire_colour[3] = 1.0f; + v3_muls( wire_colour, 0.8f, wire_colour ); + wire_colour[3] = 1.0f; - glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, wire_colour ); - } - else - { - glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, trigger_id? wire_right_colour: wire_left_colour ); - } - //glUniform4fv( SHADER_UNIFORM( shader_tile_colour, "uColour" ), - // 1, trigger_id? wire_right_colour: wire_left_colour ); - - for( int i = 0; i < 2; i ++ ) - { - glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), - pts[i][0], - pts[i][1], - 0.08f - ); - draw_mesh( filled_start, filled_count ); - } + glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, wire_colour ); + } + else + { + glUniform4fv( SHADER_UNIFORM( shader_wire, "uColour" ), 1, j? wire_right_colour: wire_left_colour ); + } + + for( int i = 0; i < 2; i ++ ) + { + glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), + pts[i][0], + pts[i][1], + 0.08f + ); + draw_mesh( filled_start, filled_count ); } } } } - + // SUB SPLITTER DIRECTION // ======================================================================================================== - + // TODO: Make this sprites probably + glUniform4f( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 0.9f, 0.35f, 0.1f, 0.75f ); - for( int y = 2; y < world.h-2; y ++ ) + for( int i = 0; i < world.tile_special_count; i ++ ) { - for( int x = 2; x < world.w-2; x ++ ) - { - struct cell *cell = pcell((v2i){x,y}); + struct render_cmd *cmd = &world.cmd_buf_specials[i]; + struct cell *cell = cmd->ptr; - if( cell->state & FLAG_CANAL && cell->state & FLAG_TARGETED && cell->config == k_cell_type_split ) - { - glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), x, y, 1.0f ); - draw_mesh( cell->state & FLAG_FLIP_FLOP? 5: 4, 1 ); - } + if( cell->state & FLAG_TARGETED && cell->config == k_cell_type_split ) + { + glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), cmd->pos[0], cmd->pos[1], 1.0f ); + draw_mesh( cell->state & FLAG_FLIP_FLOP? 5: 4, 1 ); } } diff --git a/fishladder_resources.h b/fishladder_resources.h index 82efc47..7b12859 100644 --- a/fishladder_resources.h +++ b/fishladder_resources.h @@ -124,8 +124,11 @@ vg_tex2d tex_ball_noise = { .path = "textures/bnoise.qoi" }; vg_tex2d tex_monofur = { .path = "textures/ascii.qoi", .flags = VG_TEXTURE_NO_MIP }; vg_tex2d tex_unkown = { .path = "textures/unkown.qoi" }; vg_tex2d tex_buttons = { .path = "textures/buttons.qoi" }; +vg_tex2d tex_sprites = { .path = "textures/autocombine.qoi" }; -vg_tex2d *texture_list[] = { &tex_tile_detail, &tex_tile_data, &tex_wood, &tex_ball_noise, &tex_monofur, &tex_unkown, &tex_buttons, &tex_ubuntu }; +vg_tex2d *texture_list[] = { &tex_tile_detail, &tex_tile_data, &tex_wood, &tex_ball_noise, &tex_monofur, &tex_unkown, &tex_buttons, &tex_ubuntu, &tex_sprites }; + +#include "sprites_autocombine.h" // AUDIO // =========================================================================================================== @@ -600,7 +603,6 @@ SHADER_DEFINE( shader_sdf, "out vec4 FragColor;" "" "in vec2 aTexCoords;" - "in vec4 aColour;" "" "void main()" "{" @@ -612,6 +614,40 @@ SHADER_DEFINE( shader_sdf, UNIFORMS({ "uPv", "uTexGlyphs", "uColour" }) ) +SHADER_DEFINE( shader_sprite, + + // VERTEX + "layout (location=0) in vec2 a_co;" // quad mesh + "layout (location=1) in vec4 ins_uv;" // instanced data (uv) + "layout (location=2) in vec3 ins_pos;" // position + scale + "" + "uniform mat3 uPv;" + "" + "out vec2 aTexCoords;" + "" + "void main()" + "{" + "vec2 vertex_world = ins_uv.zw * a_co * ins_pos.z + ins_pos.xy;" + "gl_Position = vec4( uPv * vec3( vertex_world, 1.0 ), 1.0 );" + "aTexCoords = ins_uv.xy + (a_co+0.5)*ins_uv.zw;" + "}", + + // FRAGMENT + "uniform sampler2D uTexMain;" + "uniform vec4 uColour;" + "out vec4 FragColor;" + "" + "in vec2 aTexCoords;" + "" + "void main()" + "{" + "vec4 glyph = texture( uTexGlyphs, aTexCoords );" + "FragColor = glyph;" + "}" + , + UNIFORMS({ "uPv", "uTexMain", "uColour" }) +) + void vg_register(void) { SHADER_INIT( shader_tile_colour ); diff --git a/phoboslab/qoi.h b/phoboslab/qoi.h index d70e2e8..988f9ed 100644 --- a/phoboslab/qoi.h +++ b/phoboslab/qoi.h @@ -28,12 +28,9 @@ SOFTWARE. -- About -QOI encodes and decodes images in a lossless format. An encoded QOI image is -usually around 10--30% larger than a decently optimized PNG image. - -QOI outperforms simpler PNG encoders in compression ratio and performance. QOI -images are typically 20% smaller than PNGs written with stbi_image. Encoding is -25-50x faster and decoding is 3-4x faster than stbi_image or libpng. +QOI encodes and decodes images in a lossless format. Compared to stb_image and +stb_image_write QOI offers 20x-50x faster encoding, 3x-4x faster decoding and +20% better compression. -- Synopsis @@ -48,7 +45,7 @@ images are typically 20% smaller than PNGs written with stbi_image. Encoding is // the input pixel data. qoi_write("image_new.qoi", rgba_pixels, &(qoi_desc){ .width = 1920, - .height = 1080, + .height = 1080, .channels = 4, .colorspace = QOI_SRGB }); @@ -77,143 +74,162 @@ QOI_NO_STDIO before including this library. This library uses malloc() and free(). To supply your own malloc implementation you can define QOI_MALLOC and QOI_FREE before including this library. +This library uses memset() to zero-initialize the index. To supply your own +implementation you can define QOI_ZEROARR before including this library. + -- Data Format -A QOI file has a 14 byte header, followed by any number of data "chunks". +A QOI file has a 14 byte header, followed by any number of data "chunks" and an +8-byte end marker. struct qoi_header_t { char magic[4]; // magic bytes "qoif" uint32_t width; // image width in pixels (BE) uint32_t height; // image height in pixels (BE) - uint8_t channels; // must be 3 (RGB) or 4 (RGBA) - uint8_t colorspace; // a bitmap 0000rgba where - // - a zero bit indicates sRGBA, - // - a one bit indicates linear (user interpreted) - // colorspace for each channel + uint8_t channels; // 3 = RGB, 4 = RGBA + uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear }; -The decoder and encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous -pixel value. Pixels are either encoded as +Images are encoded row by row, left to right, top to bottom. The decoder and +encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous pixel value. An +image is complete when all pixels specified by width * height have been covered. + +Pixels are encoded as - a run of the previous pixel - - an index into a previously seen pixel - - a difference to the previous pixel value in r,g,b,a - - full r,g,b,a values + - an index into an array of previously seen pixels + - a difference to the previous pixel value in r,g,b + - full r,g,b or r,g,b,a values + +The color channels are assumed to not be premultiplied with the alpha channel +("un-premultiplied alpha"). + +A running array[64] (zero-initialized) of previously seen pixel values is +maintained by the encoder and decoder. Each pixel that is seen by the encoder +and decoder is put into this array at the position formed by a hash function of +the color value. In the encoder, if the pixel value at the index matches the +current pixel, this index position is written to the stream as QOI_OP_INDEX. +The hash function for the index is: + + index_position = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + +Each chunk starts with a 2- or 8-bit tag, followed by a number of data bits. The +bit length of chunks is divisible by 8 - i.e. all chunks are byte aligned. All +values encoded in these data bits have the most significant bit on the left. + +The 8-bit tags have precedence over the 2-bit tags. A decoder must check for the +presence of an 8-bit tag first. -A running array[64] of previously seen pixel values is maintained by the encoder -and decoder. Each pixel that is seen by the encoder and decoder is put into this -array at the position (r^g^b^a) % 64. In the encoder, if the pixel value at this -index matches the current pixel, this index position is written to the stream. +The byte stream's end is marked with 7 0x00 bytes followed a single 0x01 byte. -Each chunk starts with a 2, 3 or 4 bit tag, followed by a number of data bits. -The bit length of chunks is divisible by 8 - i.e. all chunks are byte aligned. -All values encoded in these data bits have the most significant bit (MSB) on the -left. The possible chunks are: - - QOI_INDEX ------------- + +.- QOI_OP_INDEX ----------. | Byte[0] | | 7 6 5 4 3 2 1 0 | |-------+-----------------| | 0 0 | index | - +`-------------------------` 2-bit tag b00 6-bit index into the color index array: 0..63 - - - QOI_RUN_8 ------------- -| Byte[0] | -| 7 6 5 4 3 2 1 0 | -|----------+--------------| -| 0 1 0 | run | - -3-bit tag b010 -5-bit run-length repeating the previous pixel: 1..32 +A valid encoder must not issue 2 or more consecutive QOI_OP_INDEX chunks to the +same index. QOI_OP_RUN should be used instead. - - QOI_RUN_16 -------------------------------------- -| Byte[0] | Byte[1] | -| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | -|----------+----------------------------------------| -| 0 1 1 | run | - -3-bit tag b011 -13-bit run-length repeating the previous pixel: 33..8224 - - - - QOI_DIFF_8 ------------ +.- QOI_OP_DIFF -----------. | Byte[0] | | 7 6 5 4 3 2 1 0 | |-------+-----+-----+-----| -| 1 0 | dr | db | bg | - -2-bit tag b10 +| 0 1 | dr | dg | db | +`-------------------------` +2-bit tag b01 2-bit red channel difference from the previous pixel between -2..1 2-bit green channel difference from the previous pixel between -2..1 2-bit blue channel difference from the previous pixel between -2..1 -The difference to the current channel values are using a wraparound operation, +The difference to the current channel values are using a wraparound operation, so "1 - 2" will result in 255, while "255 + 1" will result in 0. +Values are stored as unsigned integers with a bias of 2. E.g. -2 is stored as +0 (b00). 1 is stored as 3 (b11). + +The alpha value remains unchanged from the previous pixel. - - QOI_DIFF_16 ------------------------------------- + +.- QOI_OP_LUMA -------------------------------------. | Byte[0] | Byte[1] | | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | -|----------+--------------|------------ +-----------| -| 1 1 0 | red diff | green diff | blue diff | +|-------+-----------------+-------------+-----------| +| 1 0 | green diff | dr - dg | db - dg | +`---------------------------------------------------` +2-bit tag b10 +6-bit green channel difference from the previous pixel -32..31 +4-bit red channel difference minus green channel difference -8..7 +4-bit blue channel difference minus green channel difference -8..7 -3-bit tag b110 -5-bit red channel difference from the previous pixel between -16..15 -4-bit green channel difference from the previous pixel between -8..7 -4-bit blue channel difference from the previous pixel between -8..7 +The green channel is used to indicate the general direction of change and is +encoded in 6 bits. The red and blue channels (dr and db) base their diffs off +of the green channel difference and are encoded in 4 bits. I.e.: + dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g) + db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g) -The difference to the current channel values are using a wraparound operation, +The difference to the current channel values are using a wraparound operation, so "10 - 13" will result in 253, while "250 + 7" will result in 1. +Values are stored as unsigned integers with a bias of 32 for the green channel +and a bias of 8 for the red and blue channel. - - QOI_DIFF_24 --------------------------------------------------------------- -| Byte[0] | Byte[1] | Byte[2] | -| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | -|-------------+----------------+--------------+----------------+--------------| -| 1 1 1 0 | red diff | green diff | blue diff | alpha diff | +The alpha value remains unchanged from the previous pixel. -4-bit tag b1110 -5-bit red channel difference from the previous pixel between -16..15 -5-bit green channel difference from the previous pixel between -16..15 -5-bit blue channel difference from the previous pixel between -16..15 -5-bit alpha channel difference from the previous pixel between -16..15 -The difference to the current channel values are using a wraparound operation, -so "10 - 13" will result in 253, while "250 + 7" will result in 1. - - - - QOI_COLOR ------------- +.- QOI_OP_RUN ------------. | Byte[0] | | 7 6 5 4 3 2 1 0 | -|-------------+--+--+--+--| -| 1 1 1 1 |hr|hg|hb|ha| - -4-bit tag b1111 -1-bit red byte follows -1-bit green byte follows -1-bit blue byte follows -1-bit alpha byte follows - -For each set bit hr, hg, hb and ha another byte follows in this order. If such a -byte follows, it will replace the current color channel value with the value of -this byte. - - -The byte stream is padded at the end with 4 zero bytes. Size the longest chunk -we can encounter is 5 bytes (QOI_COLOR with RGBA set), with this padding we just -have to check for an overrun once per decode loop iteration. +|-------+-----------------| +| 1 1 | run | +`-------------------------` +2-bit tag b11 +6-bit run-length repeating the previous pixel: 1..62 + +The run-length is stored with a bias of -1. Note that the run-lengths 63 and 64 +(b111110 and b111111) are illegal as they are occupied by the QOI_OP_RGB and +QOI_OP_RGBA tags. + + +.- QOI_OP_RGB ------------------------------------------. +| Byte[0] | Byte[1] | Byte[2] | Byte[3] | +| 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | +|-------------------------+---------+---------+---------| +| 1 1 1 1 1 1 1 0 | red | green | blue | +`-------------------------------------------------------` +8-bit tag b11111110 +8-bit red channel value +8-bit green channel value +8-bit blue channel value + +The alpha value remains unchanged from the previous pixel. + + +.- QOI_OP_RGBA ---------------------------------------------------. +| Byte[0] | Byte[1] | Byte[2] | Byte[3] | Byte[4] | +| 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | +|-------------------------+---------+---------+---------+---------| +| 1 1 1 1 1 1 1 1 | red | green | blue | alpha | +`-----------------------------------------------------------------` +8-bit tag b11111111 +8-bit red channel value +8-bit green channel value +8-bit blue channel value +8-bit alpha channel value */ -// ----------------------------------------------------------------------------- -// Header - Public functions +/* ----------------------------------------------------------------------------- +Header - Public functions */ #ifndef QOI_H #define QOI_H @@ -222,19 +238,20 @@ have to check for an overrun once per decode loop iteration. extern "C" { #endif -// A pointer to qoi_desc struct has to be supplied to all of qoi's functions. It -// describes either the input format (for qoi_write, qoi_encode), or is filled -// with the description read from the file header (for qoi_read, qoi_decode). +/* A pointer to a qoi_desc struct has to be supplied to all of qoi's functions. +It describes either the input format (for qoi_write and qoi_encode), or is +filled with the description read from the file header (for qoi_read and +qoi_decode). -// The colorspace in this qoi_desc is a bitmap with 0000rgba where a 0-bit -// indicates sRGB and a 1-bit indicates linear colorspace for each channel. You -// may use one of the predefined constants: QOI_SRGB, QOI_SRGB_LINEAR_ALPHA or -// QOI_LINEAR. The colorspace is purely informative. It will be saved to the -// file header, but does not affect en-/decoding in any way. +The colorspace in this qoi_desc is an enum where + 0 = sRGB, i.e. gamma scaled RGB channels and a linear alpha channel + 1 = all channels are linear +You may use the constants QOI_SRGB or QOI_LINEAR. The colorspace is purely +informative. It will be saved to the file header, but does not affect +how chunks are en-/decoded. */ -#define QOI_SRGB 0x00 -#define QOI_SRGB_LINEAR_ALPHA 0x01 -#define QOI_LINEAR 0x0f +#define QOI_SRGB 0 +#define QOI_LINEAR 1 typedef struct { unsigned int width; @@ -245,49 +262,49 @@ typedef struct { #ifndef QOI_NO_STDIO -// Encode raw RGB or RGBA pixels into a QOI image and write it to the file -// system. The qoi_desc struct must be filled with the image width, height, -// number of channels (3 = RGB, 4 = RGBA) and the colorspace. +/* Encode raw RGB or RGBA pixels into a QOI image and write it to the file +system. The qoi_desc struct must be filled with the image width, height, +number of channels (3 = RGB, 4 = RGBA) and the colorspace. -// The function returns 0 on failure (invalid parameters, or fopen or malloc -// failed) or the number of bytes written on success. +The function returns 0 on failure (invalid parameters, or fopen or malloc +failed) or the number of bytes written on success. */ int qoi_write(const char *filename, const void *data, const qoi_desc *desc); -// Read and decode a QOI image from the file system. If channels is 0, the -// number of channels from the file header is used. If channels is 3 or 4 the -// output format will be forced into this number of channels. +/* Read and decode a QOI image from the file system. If channels is 0, the +number of channels from the file header is used. If channels is 3 or 4 the +output format will be forced into this number of channels. -// The function either returns NULL on failure (invalid data, or malloc or fopen -// failed) or a pointer to the decoded pixels. On success, the qoi_desc struct -// will be filled with the description from the file header. +The function either returns NULL on failure (invalid data, or malloc or fopen +failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +will be filled with the description from the file header. -// The returned pixel data should be free()d after use. +The returned pixel data should be free()d after use. */ void *qoi_read(const char *filename, qoi_desc *desc, int channels); -#endif // QOI_NO_STDIO +#endif /* QOI_NO_STDIO */ -// Encode raw RGB or RGBA pixels into a QOI image in memory. +/* Encode raw RGB or RGBA pixels into a QOI image in memory. -// The function either returns NULL on failure (invalid parameters or malloc -// failed) or a pointer to the encoded data on success. On success the out_len -// is set to the size in bytes of the encoded data. +The function either returns NULL on failure (invalid parameters or malloc +failed) or a pointer to the encoded data on success. On success the out_len +is set to the size in bytes of the encoded data. -// The returned qoi data should be free()d after use. +The returned qoi data should be free()d after use. */ void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len); -// Decode a QOI image from memory. +/* Decode a QOI image from memory. -// The function either returns NULL on failure (invalid parameters or malloc -// failed) or a pointer to the decoded pixels. On success, the qoi_desc struct -// is filled with the description from the file header. +The function either returns NULL on failure (invalid parameters or malloc +failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +is filled with the description from the file header. -// The returned pixel data should be free()d after use. +The returned pixel data should be free()d after use. */ void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels); @@ -295,75 +312,91 @@ void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels); #ifdef __cplusplus } #endif -#endif // QOI_H +#endif /* QOI_H */ -// ----------------------------------------------------------------------------- -// Implementation +/* ----------------------------------------------------------------------------- +Implementation */ #ifdef QOI_IMPLEMENTATION #include +#include #ifndef QOI_MALLOC #define QOI_MALLOC(sz) malloc(sz) #define QOI_FREE(p) free(p) #endif +#ifndef QOI_ZEROARR + #define QOI_ZEROARR(a) memset((a),0,sizeof(a)) +#endif -#define QOI_INDEX 0x00 // 00xxxxxx -#define QOI_RUN_8 0x40 // 010xxxxx -#define QOI_RUN_16 0x60 // 011xxxxx -#define QOI_DIFF_8 0x80 // 10xxxxxx -#define QOI_DIFF_16 0xc0 // 110xxxxx -#define QOI_DIFF_24 0xe0 // 1110xxxx -#define QOI_COLOR 0xf0 // 1111xxxx +#define QOI_OP_INDEX 0x00 /* 00xxxxxx */ +#define QOI_OP_DIFF 0x40 /* 01xxxxxx */ +#define QOI_OP_LUMA 0x80 /* 10xxxxxx */ +#define QOI_OP_RUN 0xc0 /* 11xxxxxx */ +#define QOI_OP_RGB 0xfe /* 11111110 */ +#define QOI_OP_RGBA 0xff /* 11111111 */ -#define QOI_MASK_2 0xc0 // 11000000 -#define QOI_MASK_3 0xe0 // 11100000 -#define QOI_MASK_4 0xf0 // 11110000 +#define QOI_MASK_2 0xc0 /* 11000000 */ -#define QOI_COLOR_HASH(C) (C.rgba.r ^ C.rgba.g ^ C.rgba.b ^ C.rgba.a) +#define QOI_COLOR_HASH(C) (C.rgba.r*3 + C.rgba.g*5 + C.rgba.b*7 + C.rgba.a*11) #define QOI_MAGIC \ (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | \ ((unsigned int)'i') << 8 | ((unsigned int)'f')) #define QOI_HEADER_SIZE 14 -#define QOI_PADDING 4 + +/* 2GB is the max file size that this implementation can safely handle. We guard +against anything larger than that, assuming the worst case with 5 bytes per +pixel, rounded down to a nice clean value. 400 million pixels ought to be +enough for anybody. */ +#define QOI_PIXELS_MAX ((unsigned int)400000000) typedef union { struct { unsigned char r, g, b, a; } rgba; unsigned int v; } qoi_rgba_t; -void qoi_write_32(unsigned char *bytes, int *p, unsigned int v) { +static const unsigned char qoi_padding[8] = {0,0,0,0,0,0,0,1}; + +static void qoi_write_32(unsigned char *bytes, int *p, unsigned int v) { bytes[(*p)++] = (0xff000000 & v) >> 24; bytes[(*p)++] = (0x00ff0000 & v) >> 16; bytes[(*p)++] = (0x0000ff00 & v) >> 8; bytes[(*p)++] = (0x000000ff & v); } -unsigned int qoi_read_32(const unsigned char *bytes, int *p) { +static unsigned int qoi_read_32(const unsigned char *bytes, int *p) { unsigned int a = bytes[(*p)++]; unsigned int b = bytes[(*p)++]; unsigned int c = bytes[(*p)++]; unsigned int d = bytes[(*p)++]; - return (a << 24) | (b << 16) | (c << 8) | d; + return a << 24 | b << 16 | c << 8 | d; } void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { + int i, max_size, p, run; + int px_len, px_end, px_pos, channels; + unsigned char *bytes; + const unsigned char *pixels; + qoi_rgba_t index[64]; + qoi_rgba_t px, px_prev; + if ( data == NULL || out_len == NULL || desc == NULL || desc->width == 0 || desc->height == 0 || desc->channels < 3 || desc->channels > 4 || - (desc->colorspace & 0xf0) != 0 + desc->colorspace > 1 || + desc->height >= QOI_PIXELS_MAX / desc->width ) { return NULL; } - int max_size = - desc->width * desc->height * (desc->channels + 1) + - QOI_HEADER_SIZE + QOI_PADDING; + max_size = + desc->width * desc->height * (desc->channels + 1) + + QOI_HEADER_SIZE + sizeof(qoi_padding); - int p = 0; - unsigned char *bytes = QOI_MALLOC(max_size); + p = 0; + bytes = (unsigned char *) QOI_MALLOC(max_size); if (!bytes) { return NULL; } @@ -375,105 +408,98 @@ void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { bytes[p++] = desc->colorspace; - const unsigned char *pixels = (const unsigned char *)data; + pixels = (const unsigned char *)data; + + QOI_ZEROARR(index); - qoi_rgba_t index[64] = {0}; + run = 0; + px_prev.rgba.r = 0; + px_prev.rgba.g = 0; + px_prev.rgba.b = 0; + px_prev.rgba.a = 255; + px = px_prev; - int run = 0; - qoi_rgba_t px_prev = {.rgba = {.r = 0, .g = 0, .b = 0, .a = 255}}; - qoi_rgba_t px = px_prev; - - int px_len = desc->width * desc->height * desc->channels; - int px_end = px_len - desc->channels; - int channels = desc->channels; + px_len = desc->width * desc->height * desc->channels; + px_end = px_len - desc->channels; + channels = desc->channels; - for (int px_pos = 0; px_pos < px_len; px_pos += channels) { + for (px_pos = 0; px_pos < px_len; px_pos += channels) { if (channels == 4) { px = *(qoi_rgba_t *)(pixels + px_pos); } else { - px.rgba.r = pixels[px_pos]; - px.rgba.g = pixels[px_pos+1]; - px.rgba.b = pixels[px_pos+2]; + px.rgba.r = pixels[px_pos + 0]; + px.rgba.g = pixels[px_pos + 1]; + px.rgba.b = pixels[px_pos + 2]; } if (px.v == px_prev.v) { run++; + if (run == 62 || px_pos == px_end) { + bytes[p++] = QOI_OP_RUN | (run - 1); + run = 0; + } } + else { + int index_pos; - if ( - run > 0 && - (run == 0x2020 || px.v != px_prev.v || px_pos == px_end) - ) { - if (run < 33) { - run -= 1; - bytes[p++] = QOI_RUN_8 | run; + if (run > 0) { + bytes[p++] = QOI_OP_RUN | (run - 1); + run = 0; } - else { - run -= 33; - bytes[p++] = QOI_RUN_16 | run >> 8; - bytes[p++] = run; - } - run = 0; - } - if (px.v != px_prev.v) { - int index_pos = QOI_COLOR_HASH(px) % 64; + index_pos = QOI_COLOR_HASH(px) % 64; if (index[index_pos].v == px.v) { - bytes[p++] = QOI_INDEX | index_pos; + bytes[p++] = QOI_OP_INDEX | index_pos; } else { index[index_pos] = px; - int vr = px.rgba.r - px_prev.rgba.r; - int vg = px.rgba.g - px_prev.rgba.g; - int vb = px.rgba.b - px_prev.rgba.b; - int va = px.rgba.a - px_prev.rgba.a; - - if ( - vr > -17 && vr < 16 && - vg > -17 && vg < 16 && - vb > -17 && vb < 16 && - va > -17 && va < 16 - ) { + if (px.rgba.a == px_prev.rgba.a) { + signed char vr = px.rgba.r - px_prev.rgba.r; + signed char vg = px.rgba.g - px_prev.rgba.g; + signed char vb = px.rgba.b - px_prev.rgba.b; + + signed char vg_r = vr - vg; + signed char vg_b = vb - vg; + if ( - va == 0 && vr > -3 && vr < 2 && - vg > -3 && vg < 2 && + vg > -3 && vg < 2 && vb > -3 && vb < 2 ) { - bytes[p++] = QOI_DIFF_8 | ((vr + 2) << 4) | (vg + 2) << 2 | (vb + 2); + bytes[p++] = QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2); } else if ( - va == 0 && - vr > -17 && vr < 16 && - vg > -9 && vg < 8 && - vb > -9 && vb < 8 + vg_r > -9 && vg_r < 8 && + vg > -33 && vg < 32 && + vg_b > -9 && vg_b < 8 ) { - bytes[p++] = QOI_DIFF_16 | (vr + 16); - bytes[p++] = (vg + 8) << 4 | (vb + 8); + bytes[p++] = QOI_OP_LUMA | (vg + 32); + bytes[p++] = (vg_r + 8) << 4 | (vg_b + 8); } else { - bytes[p++] = QOI_DIFF_24 | (vr + 16) >> 1; - bytes[p++] = (vr + 16) << 7 | (vg + 16) << 2 | (vb + 16) >> 3; - bytes[p++] = (vb + 16) << 5 | (va + 16); + bytes[p++] = QOI_OP_RGB; + bytes[p++] = px.rgba.r; + bytes[p++] = px.rgba.g; + bytes[p++] = px.rgba.b; } } else { - bytes[p++] = QOI_COLOR | (vr ? 8 : 0) | (vg ? 4 : 0) | (vb ? 2 : 0) | (va ? 1 : 0); - if (vr) { bytes[p++] = px.rgba.r; } - if (vg) { bytes[p++] = px.rgba.g; } - if (vb) { bytes[p++] = px.rgba.b; } - if (va) { bytes[p++] = px.rgba.a; } + bytes[p++] = QOI_OP_RGBA; + bytes[p++] = px.rgba.r; + bytes[p++] = px.rgba.g; + bytes[p++] = px.rgba.b; + bytes[p++] = px.rgba.a; } } } px_prev = px; } - for (int i = 0; i < QOI_PADDING; i++) { - bytes[p++] = 0; + for (i = 0; i < (int)sizeof(qoi_padding); i++) { + bytes[p++] = qoi_padding[i]; } *out_len = p; @@ -481,27 +507,36 @@ void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { } void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { + const unsigned char *bytes; + unsigned int header_magic; + unsigned char *pixels; + qoi_rgba_t index[64]; + qoi_rgba_t px; + int px_len, chunks_len, px_pos; + int p = 0, run = 0; + if ( data == NULL || desc == NULL || (channels != 0 && channels != 3 && channels != 4) || - size < QOI_HEADER_SIZE + QOI_PADDING + size < QOI_HEADER_SIZE + (int)sizeof(qoi_padding) ) { return NULL; } - const unsigned char *bytes = (const unsigned char *)data; - int p = 0; + bytes = (const unsigned char *)data; - unsigned int header_magic = qoi_read_32(bytes, &p); + header_magic = qoi_read_32(bytes, &p); desc->width = qoi_read_32(bytes, &p); desc->height = qoi_read_32(bytes, &p); desc->channels = bytes[p++]; desc->colorspace = bytes[p++]; if ( - desc->width == 0 || desc->height == 0 || + desc->width == 0 || desc->height == 0 || desc->channels < 3 || desc->channels > 4 || - header_magic != QOI_MAGIC + desc->colorspace > 1 || + header_magic != QOI_MAGIC || + desc->height >= QOI_PIXELS_MAX / desc->width ) { return NULL; } @@ -510,70 +545,66 @@ void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { channels = desc->channels; } - int px_len = desc->width * desc->height * channels; - unsigned char *pixels = QOI_MALLOC(px_len); + px_len = desc->width * desc->height * channels; + pixels = (unsigned char *) QOI_MALLOC(px_len); if (!pixels) { return NULL; } - qoi_rgba_t px = {.rgba = {.r = 0, .g = 0, .b = 0, .a = 255}}; - qoi_rgba_t index[64] = {0}; + QOI_ZEROARR(index); + px.rgba.r = 0; + px.rgba.g = 0; + px.rgba.b = 0; + px.rgba.a = 255; - int run = 0; - int chunks_len = size - QOI_PADDING; - for (int px_pos = 0; px_pos < px_len; px_pos += channels) { + chunks_len = size - (int)sizeof(qoi_padding); + for (px_pos = 0; px_pos < px_len; px_pos += channels) { if (run > 0) { run--; } else if (p < chunks_len) { int b1 = bytes[p++]; - if ((b1 & QOI_MASK_2) == QOI_INDEX) { - px = index[b1 ^ QOI_INDEX]; + if (b1 == QOI_OP_RGB) { + px.rgba.r = bytes[p++]; + px.rgba.g = bytes[p++]; + px.rgba.b = bytes[p++]; } - else if ((b1 & QOI_MASK_3) == QOI_RUN_8) { - run = (b1 & 0x1f); + else if (b1 == QOI_OP_RGBA) { + px.rgba.r = bytes[p++]; + px.rgba.g = bytes[p++]; + px.rgba.b = bytes[p++]; + px.rgba.a = bytes[p++]; } - else if ((b1 & QOI_MASK_3) == QOI_RUN_16) { - int b2 = bytes[p++]; - run = (((b1 & 0x1f) << 8) | (b2)) + 32; + else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) { + px = index[b1]; } - else if ((b1 & QOI_MASK_2) == QOI_DIFF_8) { + else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) { px.rgba.r += ((b1 >> 4) & 0x03) - 2; px.rgba.g += ((b1 >> 2) & 0x03) - 2; px.rgba.b += ( b1 & 0x03) - 2; } - else if ((b1 & QOI_MASK_3) == QOI_DIFF_16) { - int b2 = bytes[p++]; - px.rgba.r += (b1 & 0x1f) - 16; - px.rgba.g += (b2 >> 4) - 8; - px.rgba.b += (b2 & 0x0f) - 8; - } - else if ((b1 & QOI_MASK_4) == QOI_DIFF_24) { + else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) { int b2 = bytes[p++]; - int b3 = bytes[p++]; - px.rgba.r += (((b1 & 0x0f) << 1) | (b2 >> 7)) - 16; - px.rgba.g += ((b2 & 0x7c) >> 2) - 16; - px.rgba.b += (((b2 & 0x03) << 3) | ((b3 & 0xe0) >> 5)) - 16; - px.rgba.a += (b3 & 0x1f) - 16; + int vg = (b1 & 0x3f) - 32; + px.rgba.r += vg - 8 + ((b2 >> 4) & 0x0f); + px.rgba.g += vg; + px.rgba.b += vg - 8 + (b2 & 0x0f); } - else if ((b1 & QOI_MASK_4) == QOI_COLOR) { - if (b1 & 8) { px.rgba.r = bytes[p++]; } - if (b1 & 4) { px.rgba.g = bytes[p++]; } - if (b1 & 2) { px.rgba.b = bytes[p++]; } - if (b1 & 1) { px.rgba.a = bytes[p++]; } + else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) { + run = (b1 & 0x3f); } index[QOI_COLOR_HASH(px) % 64] = px; } - if (channels == 4) { + if (channels == 4) { *(qoi_rgba_t*)(pixels + px_pos) = px; } else { - pixels[px_pos] = px.rgba.r; - pixels[px_pos+1] = px.rgba.g; - pixels[px_pos+2] = px.rgba.b; + pixels[px_pos + 0] = px.rgba.r; + pixels[px_pos + 1] = px.rgba.g; + pixels[px_pos + 2] = px.rgba.b; } } @@ -585,48 +616,56 @@ void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { int qoi_write(const char *filename, const void *data, const qoi_desc *desc) { FILE *f = fopen(filename, "wb"); + int size; + void *encoded; + if (!f) { return 0; } - int size; - void *encoded = qoi_encode(data, desc, &size); + encoded = qoi_encode(data, desc, &size); if (!encoded) { fclose(f); return 0; - } - + } + fwrite(encoded, 1, size, f); fclose(f); - + QOI_FREE(encoded); return size; } void *qoi_read(const char *filename, qoi_desc *desc, int channels) { FILE *f = fopen(filename, "rb"); + int size, bytes_read; + void *pixels, *data; + if (!f) { return NULL; } fseek(f, 0, SEEK_END); - int size = ftell(f); + size = ftell(f); + if (size <= 0) { + fclose(f); + return NULL; + } fseek(f, 0, SEEK_SET); - void *data = QOI_MALLOC(size); + data = QOI_MALLOC(size); if (!data) { fclose(f); return NULL; } - int bytes_read = fread(data, 1, size, f); + bytes_read = fread(data, 1, size, f); fclose(f); - void *pixels = qoi_decode(data, bytes_read, desc, channels); + pixels = qoi_decode(data, bytes_read, desc, channels); QOI_FREE(data); return pixels; } -#endif // QOI_NO_STDIO -#endif // QOI_IMPLEMENTATION - +#endif /* QOI_NO_STDIO */ +#endif /* QOI_IMPLEMENTATION */ diff --git a/phoboslab/qoi_old.h b/phoboslab/qoi_old.h new file mode 100644 index 0000000..d70e2e8 --- /dev/null +++ b/phoboslab/qoi_old.h @@ -0,0 +1,632 @@ +/* + +QOI - The "Quite OK Image" format for fast, lossless image compression + +Dominic Szablewski - https://phoboslab.org + + +-- LICENSE: The MIT License(MIT) + +Copyright(c) 2021 Dominic Szablewski + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files(the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions : +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +-- About + +QOI encodes and decodes images in a lossless format. An encoded QOI image is +usually around 10--30% larger than a decently optimized PNG image. + +QOI outperforms simpler PNG encoders in compression ratio and performance. QOI +images are typically 20% smaller than PNGs written with stbi_image. Encoding is +25-50x faster and decoding is 3-4x faster than stbi_image or libpng. + + +-- Synopsis + +// Define `QOI_IMPLEMENTATION` in *one* C/C++ file before including this +// library to create the implementation. + +#define QOI_IMPLEMENTATION +#include "qoi.h" + +// Encode and store an RGBA buffer to the file system. The qoi_desc describes +// the input pixel data. +qoi_write("image_new.qoi", rgba_pixels, &(qoi_desc){ + .width = 1920, + .height = 1080, + .channels = 4, + .colorspace = QOI_SRGB +}); + +// Load and decode a QOI image from the file system into a 32bbp RGBA buffer. +// The qoi_desc struct will be filled with the width, height, number of channels +// and colorspace read from the file header. +qoi_desc desc; +void *rgba_pixels = qoi_read("image.qoi", &desc, 4); + + + +-- Documentation + +This library provides the following functions; +- qoi_read -- read and decode a QOI file +- qoi_decode -- decode the raw bytes of a QOI image from memory +- qoi_write -- encode and write a QOI file +- qoi_encode -- encode an rgba buffer into a QOI image in memory + +See the function declaration below for the signature and more information. + +If you don't want/need the qoi_read and qoi_write functions, you can define +QOI_NO_STDIO before including this library. + +This library uses malloc() and free(). To supply your own malloc implementation +you can define QOI_MALLOC and QOI_FREE before including this library. + + +-- Data Format + +A QOI file has a 14 byte header, followed by any number of data "chunks". + +struct qoi_header_t { + char magic[4]; // magic bytes "qoif" + uint32_t width; // image width in pixels (BE) + uint32_t height; // image height in pixels (BE) + uint8_t channels; // must be 3 (RGB) or 4 (RGBA) + uint8_t colorspace; // a bitmap 0000rgba where + // - a zero bit indicates sRGBA, + // - a one bit indicates linear (user interpreted) + // colorspace for each channel +}; + +The decoder and encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous +pixel value. Pixels are either encoded as + - a run of the previous pixel + - an index into a previously seen pixel + - a difference to the previous pixel value in r,g,b,a + - full r,g,b,a values + +A running array[64] of previously seen pixel values is maintained by the encoder +and decoder. Each pixel that is seen by the encoder and decoder is put into this +array at the position (r^g^b^a) % 64. In the encoder, if the pixel value at this +index matches the current pixel, this index position is written to the stream. + +Each chunk starts with a 2, 3 or 4 bit tag, followed by a number of data bits. +The bit length of chunks is divisible by 8 - i.e. all chunks are byte aligned. +All values encoded in these data bits have the most significant bit (MSB) on the +left. + +The possible chunks are: + + - QOI_INDEX ------------- +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----------------| +| 0 0 | index | + +2-bit tag b00 +6-bit index into the color index array: 0..63 + + + - QOI_RUN_8 ------------- +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|----------+--------------| +| 0 1 0 | run | + +3-bit tag b010 +5-bit run-length repeating the previous pixel: 1..32 + + + - QOI_RUN_16 -------------------------------------- +| Byte[0] | Byte[1] | +| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | +|----------+----------------------------------------| +| 0 1 1 | run | + +3-bit tag b011 +13-bit run-length repeating the previous pixel: 33..8224 + + + - QOI_DIFF_8 ------------ +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----+-----+-----| +| 1 0 | dr | db | bg | + +2-bit tag b10 +2-bit red channel difference from the previous pixel between -2..1 +2-bit green channel difference from the previous pixel between -2..1 +2-bit blue channel difference from the previous pixel between -2..1 + +The difference to the current channel values are using a wraparound operation, +so "1 - 2" will result in 255, while "255 + 1" will result in 0. + + + - QOI_DIFF_16 ------------------------------------- +| Byte[0] | Byte[1] | +| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | +|----------+--------------|------------ +-----------| +| 1 1 0 | red diff | green diff | blue diff | + +3-bit tag b110 +5-bit red channel difference from the previous pixel between -16..15 +4-bit green channel difference from the previous pixel between -8..7 +4-bit blue channel difference from the previous pixel between -8..7 + +The difference to the current channel values are using a wraparound operation, +so "10 - 13" will result in 253, while "250 + 7" will result in 1. + + + - QOI_DIFF_24 --------------------------------------------------------------- +| Byte[0] | Byte[1] | Byte[2] | +| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | +|-------------+----------------+--------------+----------------+--------------| +| 1 1 1 0 | red diff | green diff | blue diff | alpha diff | + +4-bit tag b1110 +5-bit red channel difference from the previous pixel between -16..15 +5-bit green channel difference from the previous pixel between -16..15 +5-bit blue channel difference from the previous pixel between -16..15 +5-bit alpha channel difference from the previous pixel between -16..15 + +The difference to the current channel values are using a wraparound operation, +so "10 - 13" will result in 253, while "250 + 7" will result in 1. + + + - QOI_COLOR ------------- +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------------+--+--+--+--| +| 1 1 1 1 |hr|hg|hb|ha| + +4-bit tag b1111 +1-bit red byte follows +1-bit green byte follows +1-bit blue byte follows +1-bit alpha byte follows + +For each set bit hr, hg, hb and ha another byte follows in this order. If such a +byte follows, it will replace the current color channel value with the value of +this byte. + + +The byte stream is padded at the end with 4 zero bytes. Size the longest chunk +we can encounter is 5 bytes (QOI_COLOR with RGBA set), with this padding we just +have to check for an overrun once per decode loop iteration. + +*/ + + +// ----------------------------------------------------------------------------- +// Header - Public functions + +#ifndef QOI_H +#define QOI_H + +#ifdef __cplusplus +extern "C" { +#endif + +// A pointer to qoi_desc struct has to be supplied to all of qoi's functions. It +// describes either the input format (for qoi_write, qoi_encode), or is filled +// with the description read from the file header (for qoi_read, qoi_decode). + +// The colorspace in this qoi_desc is a bitmap with 0000rgba where a 0-bit +// indicates sRGB and a 1-bit indicates linear colorspace for each channel. You +// may use one of the predefined constants: QOI_SRGB, QOI_SRGB_LINEAR_ALPHA or +// QOI_LINEAR. The colorspace is purely informative. It will be saved to the +// file header, but does not affect en-/decoding in any way. + +#define QOI_SRGB 0x00 +#define QOI_SRGB_LINEAR_ALPHA 0x01 +#define QOI_LINEAR 0x0f + +typedef struct { + unsigned int width; + unsigned int height; + unsigned char channels; + unsigned char colorspace; +} qoi_desc; + +#ifndef QOI_NO_STDIO + +// Encode raw RGB or RGBA pixels into a QOI image and write it to the file +// system. The qoi_desc struct must be filled with the image width, height, +// number of channels (3 = RGB, 4 = RGBA) and the colorspace. + +// The function returns 0 on failure (invalid parameters, or fopen or malloc +// failed) or the number of bytes written on success. + +int qoi_write(const char *filename, const void *data, const qoi_desc *desc); + + +// Read and decode a QOI image from the file system. If channels is 0, the +// number of channels from the file header is used. If channels is 3 or 4 the +// output format will be forced into this number of channels. + +// The function either returns NULL on failure (invalid data, or malloc or fopen +// failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +// will be filled with the description from the file header. + +// The returned pixel data should be free()d after use. + +void *qoi_read(const char *filename, qoi_desc *desc, int channels); + +#endif // QOI_NO_STDIO + + +// Encode raw RGB or RGBA pixels into a QOI image in memory. + +// The function either returns NULL on failure (invalid parameters or malloc +// failed) or a pointer to the encoded data on success. On success the out_len +// is set to the size in bytes of the encoded data. + +// The returned qoi data should be free()d after use. + +void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len); + + +// Decode a QOI image from memory. + +// The function either returns NULL on failure (invalid parameters or malloc +// failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +// is filled with the description from the file header. + +// The returned pixel data should be free()d after use. + +void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels); + + +#ifdef __cplusplus +} +#endif +#endif // QOI_H + + +// ----------------------------------------------------------------------------- +// Implementation + +#ifdef QOI_IMPLEMENTATION +#include + +#ifndef QOI_MALLOC + #define QOI_MALLOC(sz) malloc(sz) + #define QOI_FREE(p) free(p) +#endif + +#define QOI_INDEX 0x00 // 00xxxxxx +#define QOI_RUN_8 0x40 // 010xxxxx +#define QOI_RUN_16 0x60 // 011xxxxx +#define QOI_DIFF_8 0x80 // 10xxxxxx +#define QOI_DIFF_16 0xc0 // 110xxxxx +#define QOI_DIFF_24 0xe0 // 1110xxxx +#define QOI_COLOR 0xf0 // 1111xxxx + +#define QOI_MASK_2 0xc0 // 11000000 +#define QOI_MASK_3 0xe0 // 11100000 +#define QOI_MASK_4 0xf0 // 11110000 + +#define QOI_COLOR_HASH(C) (C.rgba.r ^ C.rgba.g ^ C.rgba.b ^ C.rgba.a) +#define QOI_MAGIC \ + (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | \ + ((unsigned int)'i') << 8 | ((unsigned int)'f')) +#define QOI_HEADER_SIZE 14 +#define QOI_PADDING 4 + +typedef union { + struct { unsigned char r, g, b, a; } rgba; + unsigned int v; +} qoi_rgba_t; + +void qoi_write_32(unsigned char *bytes, int *p, unsigned int v) { + bytes[(*p)++] = (0xff000000 & v) >> 24; + bytes[(*p)++] = (0x00ff0000 & v) >> 16; + bytes[(*p)++] = (0x0000ff00 & v) >> 8; + bytes[(*p)++] = (0x000000ff & v); +} + +unsigned int qoi_read_32(const unsigned char *bytes, int *p) { + unsigned int a = bytes[(*p)++]; + unsigned int b = bytes[(*p)++]; + unsigned int c = bytes[(*p)++]; + unsigned int d = bytes[(*p)++]; + return (a << 24) | (b << 16) | (c << 8) | d; +} + +void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { + if ( + data == NULL || out_len == NULL || desc == NULL || + desc->width == 0 || desc->height == 0 || + desc->channels < 3 || desc->channels > 4 || + (desc->colorspace & 0xf0) != 0 + ) { + return NULL; + } + + int max_size = + desc->width * desc->height * (desc->channels + 1) + + QOI_HEADER_SIZE + QOI_PADDING; + + int p = 0; + unsigned char *bytes = QOI_MALLOC(max_size); + if (!bytes) { + return NULL; + } + + qoi_write_32(bytes, &p, QOI_MAGIC); + qoi_write_32(bytes, &p, desc->width); + qoi_write_32(bytes, &p, desc->height); + bytes[p++] = desc->channels; + bytes[p++] = desc->colorspace; + + + const unsigned char *pixels = (const unsigned char *)data; + + qoi_rgba_t index[64] = {0}; + + int run = 0; + qoi_rgba_t px_prev = {.rgba = {.r = 0, .g = 0, .b = 0, .a = 255}}; + qoi_rgba_t px = px_prev; + + int px_len = desc->width * desc->height * desc->channels; + int px_end = px_len - desc->channels; + int channels = desc->channels; + + for (int px_pos = 0; px_pos < px_len; px_pos += channels) { + if (channels == 4) { + px = *(qoi_rgba_t *)(pixels + px_pos); + } + else { + px.rgba.r = pixels[px_pos]; + px.rgba.g = pixels[px_pos+1]; + px.rgba.b = pixels[px_pos+2]; + } + + if (px.v == px_prev.v) { + run++; + } + + if ( + run > 0 && + (run == 0x2020 || px.v != px_prev.v || px_pos == px_end) + ) { + if (run < 33) { + run -= 1; + bytes[p++] = QOI_RUN_8 | run; + } + else { + run -= 33; + bytes[p++] = QOI_RUN_16 | run >> 8; + bytes[p++] = run; + } + run = 0; + } + + if (px.v != px_prev.v) { + int index_pos = QOI_COLOR_HASH(px) % 64; + + if (index[index_pos].v == px.v) { + bytes[p++] = QOI_INDEX | index_pos; + } + else { + index[index_pos] = px; + + int vr = px.rgba.r - px_prev.rgba.r; + int vg = px.rgba.g - px_prev.rgba.g; + int vb = px.rgba.b - px_prev.rgba.b; + int va = px.rgba.a - px_prev.rgba.a; + + if ( + vr > -17 && vr < 16 && + vg > -17 && vg < 16 && + vb > -17 && vb < 16 && + va > -17 && va < 16 + ) { + if ( + va == 0 && + vr > -3 && vr < 2 && + vg > -3 && vg < 2 && + vb > -3 && vb < 2 + ) { + bytes[p++] = QOI_DIFF_8 | ((vr + 2) << 4) | (vg + 2) << 2 | (vb + 2); + } + else if ( + va == 0 && + vr > -17 && vr < 16 && + vg > -9 && vg < 8 && + vb > -9 && vb < 8 + ) { + bytes[p++] = QOI_DIFF_16 | (vr + 16); + bytes[p++] = (vg + 8) << 4 | (vb + 8); + } + else { + bytes[p++] = QOI_DIFF_24 | (vr + 16) >> 1; + bytes[p++] = (vr + 16) << 7 | (vg + 16) << 2 | (vb + 16) >> 3; + bytes[p++] = (vb + 16) << 5 | (va + 16); + } + } + else { + bytes[p++] = QOI_COLOR | (vr ? 8 : 0) | (vg ? 4 : 0) | (vb ? 2 : 0) | (va ? 1 : 0); + if (vr) { bytes[p++] = px.rgba.r; } + if (vg) { bytes[p++] = px.rgba.g; } + if (vb) { bytes[p++] = px.rgba.b; } + if (va) { bytes[p++] = px.rgba.a; } + } + } + } + px_prev = px; + } + + for (int i = 0; i < QOI_PADDING; i++) { + bytes[p++] = 0; + } + + *out_len = p; + return bytes; +} + +void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { + if ( + data == NULL || desc == NULL || + (channels != 0 && channels != 3 && channels != 4) || + size < QOI_HEADER_SIZE + QOI_PADDING + ) { + return NULL; + } + + const unsigned char *bytes = (const unsigned char *)data; + int p = 0; + + unsigned int header_magic = qoi_read_32(bytes, &p); + desc->width = qoi_read_32(bytes, &p); + desc->height = qoi_read_32(bytes, &p); + desc->channels = bytes[p++]; + desc->colorspace = bytes[p++]; + + if ( + desc->width == 0 || desc->height == 0 || + desc->channels < 3 || desc->channels > 4 || + header_magic != QOI_MAGIC + ) { + return NULL; + } + + if (channels == 0) { + channels = desc->channels; + } + + int px_len = desc->width * desc->height * channels; + unsigned char *pixels = QOI_MALLOC(px_len); + if (!pixels) { + return NULL; + } + + qoi_rgba_t px = {.rgba = {.r = 0, .g = 0, .b = 0, .a = 255}}; + qoi_rgba_t index[64] = {0}; + + int run = 0; + int chunks_len = size - QOI_PADDING; + for (int px_pos = 0; px_pos < px_len; px_pos += channels) { + if (run > 0) { + run--; + } + else if (p < chunks_len) { + int b1 = bytes[p++]; + + if ((b1 & QOI_MASK_2) == QOI_INDEX) { + px = index[b1 ^ QOI_INDEX]; + } + else if ((b1 & QOI_MASK_3) == QOI_RUN_8) { + run = (b1 & 0x1f); + } + else if ((b1 & QOI_MASK_3) == QOI_RUN_16) { + int b2 = bytes[p++]; + run = (((b1 & 0x1f) << 8) | (b2)) + 32; + } + else if ((b1 & QOI_MASK_2) == QOI_DIFF_8) { + px.rgba.r += ((b1 >> 4) & 0x03) - 2; + px.rgba.g += ((b1 >> 2) & 0x03) - 2; + px.rgba.b += ( b1 & 0x03) - 2; + } + else if ((b1 & QOI_MASK_3) == QOI_DIFF_16) { + int b2 = bytes[p++]; + px.rgba.r += (b1 & 0x1f) - 16; + px.rgba.g += (b2 >> 4) - 8; + px.rgba.b += (b2 & 0x0f) - 8; + } + else if ((b1 & QOI_MASK_4) == QOI_DIFF_24) { + int b2 = bytes[p++]; + int b3 = bytes[p++]; + px.rgba.r += (((b1 & 0x0f) << 1) | (b2 >> 7)) - 16; + px.rgba.g += ((b2 & 0x7c) >> 2) - 16; + px.rgba.b += (((b2 & 0x03) << 3) | ((b3 & 0xe0) >> 5)) - 16; + px.rgba.a += (b3 & 0x1f) - 16; + } + else if ((b1 & QOI_MASK_4) == QOI_COLOR) { + if (b1 & 8) { px.rgba.r = bytes[p++]; } + if (b1 & 4) { px.rgba.g = bytes[p++]; } + if (b1 & 2) { px.rgba.b = bytes[p++]; } + if (b1 & 1) { px.rgba.a = bytes[p++]; } + } + + index[QOI_COLOR_HASH(px) % 64] = px; + } + + if (channels == 4) { + *(qoi_rgba_t*)(pixels + px_pos) = px; + } + else { + pixels[px_pos] = px.rgba.r; + pixels[px_pos+1] = px.rgba.g; + pixels[px_pos+2] = px.rgba.b; + } + } + + return pixels; +} + +#ifndef QOI_NO_STDIO +#include + +int qoi_write(const char *filename, const void *data, const qoi_desc *desc) { + FILE *f = fopen(filename, "wb"); + if (!f) { + return 0; + } + + int size; + void *encoded = qoi_encode(data, desc, &size); + if (!encoded) { + fclose(f); + return 0; + } + + fwrite(encoded, 1, size, f); + fclose(f); + + QOI_FREE(encoded); + return size; +} + +void *qoi_read(const char *filename, qoi_desc *desc, int channels) { + FILE *f = fopen(filename, "rb"); + if (!f) { + return NULL; + } + + fseek(f, 0, SEEK_END); + int size = ftell(f); + fseek(f, 0, SEEK_SET); + + void *data = QOI_MALLOC(size); + if (!data) { + fclose(f); + return NULL; + } + + int bytes_read = fread(data, 1, size, f); + fclose(f); + + void *pixels = qoi_decode(data, bytes_read, desc, channels); + QOI_FREE(data); + return pixels; +} + +#endif // QOI_NO_STDIO +#endif // QOI_IMPLEMENTATION + diff --git a/sprites_autocombine.h b/sprites_autocombine.h new file mode 100644 index 0000000..2808eb0 --- /dev/null +++ b/sprites_autocombine.h @@ -0,0 +1,15 @@ +static enum sprites_auto_combine_index +{ + k_sprite_donut, + k_sprite_jack_1, + k_sprite_jack_2, + k_sprite_peg, +}; + +static struct vg_sprite sprites_auto_combine[] = +{ + { 0, 0, 8192, 8192 }, + { 8192, 0, 4096, 4096 }, + { 12288, 0, 4096, 4096 }, + { 16384, 0, 4096, 4096 }, +}; \ No newline at end of file diff --git a/texsheet.c b/texsheet.c new file mode 100644 index 0000000..f420866 --- /dev/null +++ b/texsheet.c @@ -0,0 +1,214 @@ +// Copyright (C) 2021 Harry Godden (hgn) - All Rights Reserved + +#define VG_TOOLS +#include "vg/vg.h" + +#define STB_IMAGE_IMPLEMENTATION +#include "stb/stb_image.h" + +struct image_src +{ + int x,y,ch; + + u8 *data; +}; + +int image_sort( const void* a, const void* b) +{ + struct image_src *p_a = (struct image_src *)a; + struct image_src *p_b = (struct image_src *)b; + + if( p_a->x == p_b->x ) + return 0; + else if ( p_a->x < p_b->x ) + return 1; + else + return -1; +} + +int main( int argc, const char *argv[] ) +{ + struct image_src *source_images = malloc( sizeof( struct image_src ) * argc ); + + u32 num_images = 0; + + if( argc < 4 ) + { + vg_error( "Missing output file paths\n" ); + return 0; + } + + // Open header handle + // ------------------ + FILE *fp = fopen( argv[2], "w" ); + if( !fp ) + { + vg_error( "Could not open file for writing\n" ); + return 0; + } + + fprintf( fp, "static enum %s_index\n{\n", argv[3] ); + + // Load images + // ----------- + stbi_set_flip_vertically_on_load(1); + + for( int i = 4; i < argc; i ++ ) + { + struct image_src *src = &source_images[ num_images ]; + src->data = (u8 *)stbi_load( argv[i], &src->x, &src->y, &src->ch, 4 ); + + char name[ 256 ]; + int j = 0; int ext = 0; + for( ; j < vg_list_size( name )-1; j ++ ) + { + if( argv[i][j] ) + { + name[j] = argv[i][j]; + + if( name[j] == '.' ) + ext = j; + + if( name[j] == '.' || name[j] == '-' ) + name[j] = '_'; + } + else + break; + } + + if( ext ) + name[ext] = 0x00; + else + name[j] = 0x00; + + fprintf( fp, "\tk_sprite_%s,\n", name ); + + if( src->data ) + { + if( src->x != src->y ) + { + vg_error( "Non-square images are currently not supported ('%s')\n", argv[i] ); + free( src->data ); + } + else + num_images ++; + } + else + vg_error( "Could not decode '%s'\n", argv[i] ); + } + + fprintf( fp, "};\n\n" ); + + // Sort by size + // ------------ + qsort( source_images, num_images, sizeof(struct image_src), image_sort ); + + // Process images + // -------------- + fprintf( fp, "static struct vg_sprite %s[] = \n{\n", argv[3] ); + + u8 *dest = (u8 *)malloc( 1024*1024*4 ); + + // Clear (temp) + for( int i = 0; i < 1024*1024; i ++ ) + { + dest[ i*4 + 0 ] = 0; + dest[ i*4 + 1 ] = 0; + dest[ i*4 + 2 ] = 128; + dest[ i*4 + 3 ] = 255; + } + + struct region + { + v2i p0; + v2i p1; + } + region_stack[ 32 ] = + { + { + .p0 = { 0, 0 }, + .p1 = { 1024, 1024 } + } + }; + int stack_h = 0; + + int sf = 64; + + for( int i = 0; i < num_images; i ++ ) + { + struct image_src *psrc = &source_images[ i ]; + + // Region checks + while( 1 ) + { + struct region *pregion = ®ion_stack[ stack_h ]; + + if( (pregion->p0[ 0 ] + psrc->x <= pregion->p1[0]) && (pregion->p0[ 1 ] + psrc->y <= pregion->p1[1]) ) + { + // Passed, add image and create subdivisions + fprintf( fp, "\t{ %hu, %hu, %hu, %hu },\n", + (u16)(sf * pregion->p0[0]), + (u16)(sf * pregion->p0[1]), + (u16)(sf * psrc->x), + (u16)(sf * psrc->y) + ); + + // Write image + for( int y = 0; y < psrc->y; y ++ ) + { + int px = pregion->p0[0]; + int py = pregion->p0[1] + y; + + memcpy( &dest[ (py*1024+px) * 4 ], &psrc->data[ y*psrc->x*4 ], psrc->x*4 ); + } + + // Subdivisions + stack_h ++; + struct region *new_region = ®ion_stack[ stack_h ]; + + new_region->p0[0] = pregion->p0[0] + psrc->x; + new_region->p0[1] = pregion->p0[1]; + new_region->p1[0] = pregion->p1[0]; + new_region->p1[1] = pregion->p0[1] + psrc->y; + + pregion->p0[ 1 ] += psrc->y; + break; + } + else + { + // Failed, loop up to next region if can + if( stack_h == 0 ) + { + vg_error( "Could not fit image %d. Pack failed\n", i ); + + goto IL_END_ERR; + } + else + stack_h --; + } + } + } + +IL_END_ERR: + fprintf( fp, "};" ); + fclose( fp ); + + // Write output + // ------------ + qoi_write( argv[1], dest, &(qoi_desc){ + .width = 1024, + .height = 1024, + .channels = 4, + .colorspace = QOI_SRGB + }); + + // Free + // ---- + for( int i = 0; i < num_images; i ++ ) + free( source_images[ i ].data ); + free( dest ); + free( source_images ); + + vg_success( "Processed %u images\n", num_images ); + +} diff --git a/textures_combine/donut.png b/textures_combine/donut.png new file mode 100644 index 0000000..a585c5e Binary files /dev/null and b/textures_combine/donut.png differ diff --git a/textures_combine/jack-1.png b/textures_combine/jack-1.png new file mode 100644 index 0000000..7d88b87 Binary files /dev/null and b/textures_combine/jack-1.png differ diff --git a/textures_combine/jack-2.png b/textures_combine/jack-2.png new file mode 100644 index 0000000..92488da Binary files /dev/null and b/textures_combine/jack-2.png differ diff --git a/textures_combine/peg.png b/textures_combine/peg.png new file mode 100644 index 0000000..83b366b Binary files /dev/null and b/textures_combine/peg.png differ diff --git a/vg/vg_tex.h b/vg/vg_tex.h index e97900f..c757b18 100644 --- a/vg/vg_tex.h +++ b/vg/vg_tex.h @@ -12,6 +12,13 @@ struct vg_tex2d GLuint name; }; +#pragma pack(push,1) +struct vg_sprite +{ + u16 uvx, uvy, w, h; +}; +#pragma pack(pop) + static void vg_tex2d_bind( vg_tex2d *tex, u32 id ) { glActiveTexture( GL_TEXTURE0 + id );