v / vlib / gg / image.c.v
493 lines · 448 sloc · 14.72 KB · ef5d8d62ac402f9aa1fca35e12dc7ab91022ec23
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3module gg
4
5import os
6import stbi
7import sokol.gfx
8import sokol.sgl
9
10// Image holds the fields and data needed to
11// represent a bitmap/pixel based image in memory.
12@[heap; markused]
13pub struct Image {
14pub mut:
15 id int
16 width int
17 height int
18 nr_channels int
19 nr_mipmaps int
20 ok bool
21 data voidptr
22 ext string
23 simg_ok bool
24 simg gfx.Image
25 ssmp gfx.Sampler
26 path string
27 texture_filter TextureFilter = .linear
28}
29
30@[params]
31pub struct ImageConfig {
32pub:
33 texture_filter ?TextureFilter
34 max_mipmaps int = 1 // a value below 1 means as many as possible
35}
36
37// destroy GPU resources associated with the image
38fn (image &Image) destroy() {
39 if image.ok {
40 if image.simg.id > 0 {
41 gfx.destroy_image(image.simg)
42 }
43 if image.ssmp.id > 0 {
44 gfx.destroy_sampler(image.ssmp)
45 }
46 }
47}
48
49// create_image_with_filter creates an `Image` from `file` with the requested texture filter.
50@[deprecated: 'use Context.create_image instead']
51pub fn (mut ctx Context) create_image_with_filter(file string, texture_filter TextureFilter) !Image {
52 return ctx.create_image(file, texture_filter: texture_filter)!
53}
54
55// create_image creates an `Image` from `file`.
56pub fn (mut ctx Context) create_image(file string, cfg ImageConfig) !Image {
57 if !os.exists(file) {
58 $if android {
59 image_data := os.read_apk_asset(file)!
60 mut image := ctx.create_image_from_byte_array(image_data, cfg)!
61
62 image.path = file
63
64 return image
65 } $else {
66 return error('image file "${file}" not found')
67 }
68 }
69 $if macos {
70 if ctx.native_rendering {
71 mut img := C.darwin_create_image(file)
72 img.texture_filter = cfg.texture_filter or { ctx.config.texture_filter }
73
74 img.id = ctx.image_cache.len
75 unsafe {
76 ctx.image_cache << img
77 }
78 return img
79 }
80 }
81
82 mut img := load_image(file)!
83 img.id = ctx.image_cache.len
84 img.texture_filter = cfg.texture_filter or { ctx.config.texture_filter }
85 unsafe {
86 ctx.image_cache << img
87 }
88 if gfx.is_valid() {
89 img.init_sokol_image()
90 }
91 return img
92}
93
94// adapted from https://github.com/floooh/sokol/issues/102#issuecomment-2926566603
95fn sokol_mipmap(mut simg_desc gfx.ImageDesc, max_mipmaps int) {
96 mut levels := 0
97 mut size := 0
98 width := simg_desc.width
99 height := simg_desc.height
100 mipmaps := if max_mipmaps < gfx.sg_max_mipmaps && max_mipmaps > 0 {
101 max_mipmaps
102 } else {
103 gfx.sg_max_mipmaps
104 }
105 for i in 1 .. mipmaps {
106 w := width >> i
107 h := height >> i
108 if w < 1 || h < 1 { break
109 }
110 size += w * h * 4 // 4 = img.nr_channels
111 levels++
112 }
113 // use unsafe to access the data because we have to anyway
114 mut target := unsafe { malloc(size) }
115 mut src_width := width
116 mut src_height := height
117 for level in 1 .. levels {
118 src := &u8(simg_desc.data.subimage[0][level - 1].ptr)
119 target_width := src_width / 2
120 target_height := src_height / 2
121 for x in 0 .. target_width {
122 for y in 0 .. target_height {
123 // channels RGBA
124 chans := 4
125 for ch in 0 .. 4 {
126 // avererage colors in a 2x2 square over the source image where (x*2|y*2) is the lower left corner
127 mut color := 0
128 sx := x * 2
129 sy := y * 2
130 color += unsafe { src[(sx + src_width * sy) * chans + ch] }
131 color += unsafe { src[(sx + src_width * (sy + 1)) * chans + ch] }
132 color += unsafe { src[((sx + 1) + src_width * (sy + 1)) * chans + ch] }
133 color += unsafe { src[((sx + 1) + src_width * sy) * chans + ch] }
134 color /= 4
135 unsafe {
136 target[(x + y * target_width) * chans + ch] = u8(color)
137 }
138 }
139 }
140 }
141 src_width = target_width
142 src_height = target_height
143 simg_desc.data.subimage[0][level].ptr = target
144 simg_desc.data.subimage[0][level].size = usize(target_width * target_height * 4)
145 unsafe {
146 target += target_width * target_height * 4
147 }
148 }
149 simg_desc.num_mipmaps = levels
150}
151
152// init_sokol_image initializes this `Image` for use with the
153// sokol graphical backend system.
154pub fn (mut img Image) init_sokol_image() &Image {
155 // println('\n init sokol image ${img.path} ok=${img.simg_ok}')
156 mut img_desc := gfx.ImageDesc{
157 width: img.width
158 height: img.height
159 // wrap_u: .clamp_to_edge // XTODO SAMPLER
160 // wrap_v: .clamp_to_edge
161 label: &char(img.path.str)
162 d3d11_texture: 0
163 }
164
165 // NOTE the following code, sometimes, result in hard-to-detect visual errors/bugs:
166 // img_size := usize(img.nr_channels * img.width * img.height)
167 // As an example see https://github.com/vlang/vab/issues/239
168 // The image will come out blank for some reason and no SOKOL_ASSERT
169 // nor any CI check will/can currently catch this.
170 // Since all of gg currently runs with more or less *defaults* from sokol_gfx/sokol_gl
171 // we should currently just use the sum of each of the RGB and A channels (= 4) here instead.
172 // Optimized PNG images that have no alpha channel is often optimized to only have
173 // 3 (or less) channels which stbi will correctly detect and set as `img.nr_channels`
174 // but the current sokol_gl context setup expects 4. It *should* be the same with
175 // all other stbi supported formats.
176 img_size := usize(4 * img.width * img.height)
177 img_desc.data.subimage[0][0] = gfx.Range{
178 ptr: img.data
179 size: img_size
180 }
181 sokol_mipmap(mut img_desc, img.nr_mipmaps)
182 img.nr_mipmaps = img_desc.num_mipmaps
183 img.simg = gfx.make_image(&img_desc)
184 gfx_filter := img.texture_filter.gfx_filter()
185
186 mut smp_desc := gfx.SamplerDesc{
187 min_filter: gfx_filter
188 mag_filter: gfx_filter
189 mipmap_filter: .linear
190 wrap_u: .clamp_to_edge
191 wrap_v: .clamp_to_edge
192 }
193
194 img.ssmp = gfx.make_sampler(&smp_desc)
195
196 img.simg_ok = true
197 img.ok = true
198 return img
199}
200
201// draw_image draws the provided image onto the screen.
202pub fn (ctx &Context) draw_image(x f32, y f32, width f32, height f32, img_ &Image) {
203 ctx.draw_image_with_config(
204 img: img_
205 img_rect: Rect{x, y, width, height}
206 part_rect: Rect{0, 0, img_.width, img_.height}
207 )
208}
209
210// new_streaming_image returns a cached `image_idx` of a special image, that
211// can be updated *each frame* by calling: gg.update_pixel_data(image_idx, buf)
212// ... where buf is a pointer to the actual pixel data for the image.
213// Note: you still need to call app.gg.draw_image after that, to actually draw it.
214// Note: Sokol needs to be setup, *before* calling this function. In practice,
215// this often means, that you have to call it once in the `init_fn` callback of
216// gg.new_context, or gg.start, and then store the result in your app instance.
217pub fn (mut ctx Context) new_streaming_image(w int, h int, channels int, sicfg StreamingImageConfig) int {
218 mut img := Image{}
219 img.width = w
220 img.height = h
221 img.nr_channels = channels // 4 bytes per pixel for .rgba8, see pixel_format
222 mut img_desc := gfx.ImageDesc{
223 width: img.width
224 height: img.height
225 pixel_format: sicfg.pixel_format
226 num_slices: 1
227 num_mipmaps: 1
228 usage: .stream
229 label: &char(img.path.str)
230 }
231 // Sokol requires that streamed images have NO .ptr/.size initially:
232 img_desc.data.subimage[0][0] = gfx.Range{
233 ptr: 0
234 size: usize(0)
235 }
236 img.simg = gfx.make_image(&img_desc)
237
238 mut smp_desc := gfx.SamplerDesc{
239 wrap_u: sicfg.wrap_u // SAMPLER
240 wrap_v: sicfg.wrap_v
241 min_filter: sicfg.min_filter
242 mag_filter: sicfg.mag_filter
243 }
244
245 img.ssmp = gfx.make_sampler(&smp_desc)
246 img.simg_ok = true
247 img.ok = true
248 img_idx := ctx.cache_image(img)
249 return img_idx
250}
251
252// update_pixel_data is a helper for working with image streams (i.e. images,
253// that are updated dynamically by the CPU on each frame)
254pub fn (mut ctx Context) update_pixel_data(cached_image_idx int, buf &u8) {
255 mut image := ctx.get_cached_image_by_idx(cached_image_idx)
256 image.update_pixel_data(buf)
257}
258
259// update_pixel_data updates the sokol specific pixel data associated
260// with this `Image`.
261pub fn (mut img Image) update_pixel_data(buf &u8) {
262 mut data := gfx.ImageData{}
263 data.subimage[0][0].ptr = buf
264 data.subimage[0][0].size = usize(img.width * img.height * img.nr_channels)
265 gfx.update_image(img.simg, &data)
266}
267
268// create_image_with_size creates an `Image` from `file` in the given
269// `width` x `height` dimension.
270//
271// TODO: copypasta
272@[deprecated]
273pub fn (mut ctx Context) create_image_with_size(file string, width int, height int) Image {
274 if !gfx.is_valid() {
275 // Sokol is not initialized yet, add stbi object to a queue/cache
276 // ctx.image_queue << file
277 mut img := load_image(file) or { return Image{} }
278 img.texture_filter = ctx.config.texture_filter
279 img.width = width
280 img.height = height
281 img.ok = false
282 img.id = ctx.image_cache.len
283 ctx.image_cache << img
284 return img
285 }
286 return ctx.create_image(file) or { Image{} }
287}
288
289fn load_image(file string) !Image {
290 if !os.exists(file) {
291 return error('image file "${file}" not found')
292 }
293 stb_img := stbi.load(file)!
294 return Image{
295 width: stb_img.width
296 height: stb_img.height
297 nr_channels: stb_img.nr_channels
298 ok: stb_img.ok
299 data: stb_img.data
300 ext: stb_img.ext
301 path: file
302 }
303}
304
305// create_image_from_memory creates an `Image` from the
306// memory buffer `buf` of size `bufsize`.
307//
308// See also: create_image_from_byte_array
309pub fn (mut ctx Context) create_image_from_memory(buf &u8, bufsize int, cfg ImageConfig) !Image {
310 stb_img := stbi.load_from_memory(buf, bufsize)!
311 mut img := Image{
312 width: stb_img.width
313 height: stb_img.height
314 nr_channels: stb_img.nr_channels
315 ok: stb_img.ok
316 data: stb_img.data
317 ext: stb_img.ext
318 id: ctx.image_cache.len
319 texture_filter: cfg.texture_filter or { ctx.config.texture_filter }
320 }
321 if gfx.is_valid() {
322 img.init_sokol_image()
323 }
324 ctx.image_cache << img
325 return img
326}
327
328// create_image_from_memory_with_filter creates an `Image` from `buf` with the requested texture filter.
329@[deprecated: 'use Context.create_image_from_memory instead']
330pub fn (mut ctx Context) create_image_from_memory_with_filter(buf &u8, bufsize int, texture_filter TextureFilter) !Image {
331 return ctx.create_image_from_memory(buf, bufsize, texture_filter: texture_filter)
332}
333
334// create_image_from_byte_array creates an `Image` from the
335// byte array `b`.
336//
337// See also: create_image_from_memory
338pub fn (mut ctx Context) create_image_from_byte_array(b []u8, cfg ImageConfig) !Image {
339 return ctx.create_image_from_memory(b.data, b.len, cfg)
340}
341
342// create_image_from_byte_array_with_filter creates an `Image` from `b` with the requested texture filter.
343@[deprecated: 'use Context.create_image_from_byte_array instead']
344pub fn (mut ctx Context) create_image_from_byte_array_with_filter(b []u8, texture_filter TextureFilter) !Image {
345 return ctx.create_image_from_memory(b.data, b.len, texture_filter: texture_filter)
346}
347
348pub struct StreamingImageConfig {
349pub:
350 pixel_format gfx.PixelFormat = .rgba8
351 wrap_u gfx.Wrap = .clamp_to_edge
352 wrap_v gfx.Wrap = .clamp_to_edge
353 min_filter gfx.Filter = .linear
354 mag_filter gfx.Filter = .linear
355 num_mipmaps int = 1
356 num_slices int = 1
357}
358
359fn (filter TextureFilter) gfx_filter() gfx.Filter {
360 return match filter {
361 .linear { .linear }
362 .nearest { .nearest }
363 }
364}
365
366// draw_image_with_config takes in a config that details how the
367// provided image should be drawn onto the screen
368pub fn (ctx &Context) draw_image_with_config(config DrawImageConfig) {
369 $if macos {
370 if ctx.native_rendering {
371 unsafe {
372 mut img := config.img
373 if config.img == nil {
374 // Get image by id
375 if config.img_id > 0 {
376 img = &ctx.image_cache[config.img_id]
377 } else {
378 $if !noggverbose ? {
379 eprintln('gg: failed to get image to draw natively')
380 }
381 return
382 }
383 }
384 if img.id >= ctx.image_cache.len {
385 eprintln('gg: draw_image() bad img id ${img.id} (img cache len = ${ctx.image_cache.len})')
386 return
387 }
388 if img.width == 0 {
389 println('gg: draw_image() width=0')
390 return
391 }
392 if !os.exists(img.path) {
393 println('not exist path')
394 return
395 }
396 x := config.img_rect.x
397 y := config.img_rect.y
398 width := if config.img_rect.width == 0 {
399 // Calculate the width by dividing it by the height ratio.
400 // e.g. the original image is 100x100, we're drawing 0x20. Find the ratio (5)
401 // by dividing the height 100 by 20, and then divide the width by 5.
402 f32(img.width / (img.height / config.img_rect.height))
403 } else {
404 config.img_rect.width
405 }
406 height := if config.img_rect.height == 0 {
407 // Same as above.
408 f32(img.height / (img.width / config.img_rect.width))
409 } else {
410 config.img_rect.height
411 }
412 C.darwin_draw_image(x, ctx.height - (y + config.img_rect.height), width, height,
413 img)
414 return
415 }
416 }
417 }
418
419 id := if !isnil(config.img) { config.img.id } else { config.img_id }
420 if id >= ctx.image_cache.len {
421 eprintln('gg: draw_image() bad img id ${id} (img cache len = ${ctx.image_cache.len})')
422 return
423 }
424
425 img := &ctx.image_cache[id]
426 if !img.simg_ok {
427 return
428 }
429
430 mut img_rect := config.img_rect
431 if img_rect.width == 0 && img_rect.height == 0 {
432 img_rect = Rect{img_rect.x, img_rect.y, img.width, img.height}
433 }
434
435 mut part_rect := config.part_rect
436 if part_rect.width == 0 && part_rect.height == 0 {
437 part_rect = Rect{part_rect.x, part_rect.y, img.width, img.height}
438 }
439
440 u0 := part_rect.x / img.width
441 v0 := part_rect.y / img.height
442 u1 := (part_rect.x + part_rect.width) / img.width
443 v1 := (part_rect.y + part_rect.height) / img.height
444 x0 := img_rect.x * ctx.scale
445 y0 := img_rect.y * ctx.scale
446 x1 := (img_rect.x + img_rect.width) * ctx.scale
447 mut y1 := (img_rect.y + img_rect.height) * ctx.scale
448 if img_rect.height == 0 {
449 scale := f32(img.width) / f32(img_rect.width)
450 y1 = f32(img_rect.y + int(f32(img.height) / scale)) * ctx.scale
451 }
452
453 flip_x := config.flip_x
454 flip_y := config.flip_y
455
456 mut u0f := if !flip_x { u0 } else { u1 }
457 mut u1f := if !flip_x { u1 } else { u0 }
458 mut v0f := if !flip_y { v0 } else { v1 }
459 mut v1f := if !flip_y { v1 } else { v0 }
460
461 // FIXME: is this okay?
462 match config.effect {
463 .alpha { sgl.load_pipeline(ctx.pipeline.alpha) }
464 .add { sgl.load_pipeline(ctx.pipeline.add) }
465 }
466
467 sgl.enable_texture()
468 sgl.texture(img.simg, img.ssmp)
469
470 if config.rotation != 0 {
471 width := img_rect.width * ctx.scale
472 height := (if img_rect.height > 0 { img_rect.height } else { img.height }) * ctx.scale
473
474 sgl.push_matrix()
475 sgl.translate(x0 + (width / 2), y0 + (height / 2), 0)
476 sgl.rotate(sgl.rad(-config.rotation), 0, 0, 1)
477 sgl.translate(-x0 - (width / 2), -y0 - (height / 2), 0)
478 }
479
480 sgl.begin_quads()
481 sgl.c4b(config.color.r, config.color.g, config.color.b, config.color.a)
482 sgl.v3f_t2f(x0, y0, config.z, u0f, v0f)
483 sgl.v3f_t2f(x1, y0, config.z, u1f, v0f)
484 sgl.v3f_t2f(x1, y1, config.z, u1f, v1f)
485 sgl.v3f_t2f(x0, y1, config.z, u0f, v1f)
486 sgl.end()
487
488 if config.rotation != 0 {
489 sgl.pop_matrix()
490 }
491
492 sgl.disable_texture()
493}
494