Skip to content

Commit 260caa4

Browse files
release: 0.4.2 — fix canvas rendering artifacts and scrub flicker
- Canvas.to_ansi_with_defaults: always emit SGR at col 0 so each row is self-contained; prevents apply_bg_fill from bleeding the wrong bg into the first character of rows where style carried unchanged from the previous row - Canvas_widget / Miaou Invaders: remove the 36-row cap on canvas height so the game fills the full terminal, eliminating black bars on tall terminals - Matrix driver: stop calling force_render from the main loop (modal transitions and periodic scrub now only call mark_all_dirty); this eliminates the interleaved-write race that caused visible flicker - Miaou Invaders rendering: all draw_text calls now carry an explicit bg matching the current game or HUD background, fixing black horizontal bars where sprites appeared - Scrub interval default: 30 → 300 frames (5 s at 60 fps) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7bc4e69 commit 260caa4

File tree

15 files changed

+103
-63
lines changed

15 files changed

+103
-63
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.4.2] - Unreleased
9+
10+
### Fixed
11+
12+
- **Canvas ANSI row isolation**: `Canvas.to_ansi_with_defaults` now always emits an SGR sequence at column 0 of every row, making each row self-contained. Previously, style was carried across row boundaries as an optimisation; this caused `apply_bg_fill` to bleed the wrong background into the first character of rows where style happened to carry unchanged from the previous row.
13+
- **Canvas widget fills full terminal height**: Miaou Invaders (and any `Canvas_widget` page) no longer shows black bars below the canvas on tall terminals. The 36-row cap on the canvas height has been removed so the game scales to the full terminal height.
14+
- **Matrix driver scrub flicker**: `force_render` is no longer called from the main loop (neither on modal transitions nor during periodic scrub). Both cases now only call `mark_all_dirty`, letting the render domain (the sole terminal writer) pick up the change within one frame. This eliminates the interleaved-write race that caused visible flicker.
15+
- **Miaou Invaders background**: All `draw_text` calls in the Invaders demo now carry an explicit `bg` matching the current game or HUD background. Previously, entities drawn with `bg=-1` clobbered the `fill_rect`-painted background, producing black horizontal bars wherever sprites appeared.
16+
- **Periodic scrub interval**: Default `scrub_interval_frames` reduced from 30 frames (0.5 s at 60 fps) to 300 frames (5 s), making the occasional full-refresh nearly imperceptible.
17+
818
## [0.4.1] - Unreleased
919

1020
### Fixed

example/demos/miaou_invaders/control.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ let register_timers () =
1919
timer.set_interval ~id:"alien_shoot" 0.8
2020

2121
let view s ~focus:_ ~size =
22-
let rows = max 22 (min 36 (size.LTerm_geom.rows - 1)) in
22+
let rows = max 22 (size.LTerm_geom.rows - 1) in
2323
let game_rows = max 16 (rows - 5) in
2424
let cols = max 24 (min size.LTerm_geom.cols max_cols) in
2525
let s =

example/demos/miaou_invaders/render.ml

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ open Model
1111

1212
(* -- Rendering ---------------------------------------------------------- *)
1313

14-
let style_of fg = {C.default_style with fg}
14+
(* Style helpers with explicit background so draw_text calls don't clobber
15+
the background set by fill_rect (canvas cells are fully explicit: every
16+
set_char overwrites fg, bg, and char, so bg=-1 erases the filled bg). *)
1517

16-
let bold_style_of fg = {C.default_style with fg; bold = true}
18+
let style_of ?(bg = -1) fg = {C.default_style with fg; bg}
19+
20+
let bold_style_of ?(bg = -1) fg = {C.default_style with fg; bg; bold = true}
1721

1822
let draw_background s c =
1923
let rows = C.rows c in
@@ -88,23 +92,39 @@ let draw_title_screen s c =
8892
let sub_col = max 1 ((cols - String.length sub) / 2) in
8993
let start_col = max 1 ((cols - String.length start) / 2) in
9094
let demo_col = max 1 ((cols - String.length demo) / 2) in
95+
let title_bg = 17 in
9196
let base_row = max 4 ((rows / 2) - 3) in
92-
C.draw_text c ~row:base_row ~col:title_col ~style:(bold_style_of 123) title ;
93-
C.draw_text c ~row:(base_row + 2) ~col:sub_col ~style:(style_of 153) sub ;
97+
C.draw_text
98+
c
99+
~row:base_row
100+
~col:title_col
101+
~style:(bold_style_of ~bg:title_bg 123)
102+
title ;
103+
C.draw_text
104+
c
105+
~row:(base_row + 2)
106+
~col:sub_col
107+
~style:(style_of ~bg:title_bg 153)
108+
sub ;
94109
if s.frame / 20 mod 2 = 0 then
95110
C.draw_text
96111
c
97112
~row:(base_row + 4)
98113
~col:start_col
99-
~style:(bold_style_of 229)
114+
~style:(bold_style_of ~bg:title_bg 229)
100115
start ;
101-
C.draw_text c ~row:(base_row + 6) ~col:demo_col ~style:(style_of 151) demo ;
116+
C.draw_text
117+
c
118+
~row:(base_row + 6)
119+
~col:demo_col
120+
~style:(style_of ~bg:title_bg 151)
121+
demo ;
102122
let footer = "Esc:back" in
103123
C.draw_text
104124
c
105125
~row:(rows - 1)
106126
~col:(max 1 (cols - 10))
107-
~style:(style_of 240)
127+
~style:(style_of ~bg:title_bg 240)
108128
footer
109129

110130
let camera_offset s =
@@ -121,6 +141,11 @@ let draw_game s c =
121141
if s.show_title then draw_title_screen s c
122142
else begin
123143
let cam_x, cam_y = camera_offset s in
144+
let theme = theme_for_level s.level in
145+
(* Theme-bg-aware style helpers: every canvas cell must carry an explicit
146+
background so draw_text doesn't clobber the fill_rect background. *)
147+
let gst fg = style_of ~bg:theme.bg fg in
148+
let gbst fg = bold_style_of ~bg:theme.bg fg in
124149
C.clear c ;
125150
draw_background s c ;
126151

@@ -131,9 +156,8 @@ let draw_game s c =
131156
~width:cols
132157
~height:rows
133158
~border:Single
134-
~style:(style_of 240) ;
159+
~style:(gst 240) ;
135160

136-
let theme = theme_for_level s.level in
137161
let kind = shot_kind_of s.shot_color in
138162
let power = if s.shot_power = 0 then "-" else string_of_int s.shot_power in
139163
let weapon_fg =
@@ -169,22 +193,22 @@ let draw_game s c =
169193
let col = max 1 ((cols - String.length msg) / 2) in
170194
let sub_col = max 1 ((cols - String.length sub) / 2) in
171195
let row = rows / 2 in
172-
C.draw_text c ~row ~col ~style:(bold_style_of 226) msg ;
173-
C.draw_text c ~row:(row + 1) ~col:sub_col ~style:(style_of 250) sub
196+
C.draw_text c ~row ~col ~style:(gbst 226) msg ;
197+
C.draw_text c ~row:(row + 1) ~col:sub_col ~style:(gst 250) sub
174198
| Wave_clear anim ->
175199
let fg = if Anim.value anim > 0.5 then 46 else 120 in
176200
let msg = Printf.sprintf "WAVE %d CLEAR" s.level in
177201
let col = max 1 ((cols - String.length msg) / 2) in
178-
C.draw_text c ~row:(rows / 2) ~col ~style:(bold_style_of fg) msg
202+
C.draw_text c ~row:(rows / 2) ~col ~style:(gbst fg) msg
179203
| Game_over anim ->
180204
let fg = if Anim.value anim > 0.5 then 196 else 203 in
181205
let msg = "GAME OVER" in
182206
let col = max 1 ((cols - String.length msg) / 2) in
183207
let row = rows / 2 in
184-
C.draw_text c ~row ~col ~style:(bold_style_of fg) msg ;
208+
C.draw_text c ~row ~col ~style:(gbst fg) msg ;
185209
let sub = "Press 'r' to restart" in
186210
let sub_col = max 1 ((cols - String.length sub) / 2) in
187-
C.draw_text c ~row:(row + 1) ~col:sub_col ~style:(style_of 245) sub) ;
211+
C.draw_text c ~row:(row + 1) ~col:sub_col ~style:(gst 245) sub) ;
188212

189213
(match
190214
List.find_opt (fun (a : alien) -> a.alive && a.kind = Boss) s.aliens
@@ -201,12 +225,12 @@ let draw_game s c =
201225
let bar = "[" ^ String.make filled '#' ^ String.make empty '-' ^ "]" in
202226
let label = Printf.sprintf " BOSS HP %d/%d " boss.hp boss.hp_max in
203227
let col = max 1 ((cols - String.length bar) / 2) in
204-
C.draw_text c ~row:1 ~col ~style:(bold_style_of 196) bar ;
228+
C.draw_text c ~row:1 ~col ~style:(gbst 196) bar ;
205229
C.draw_text
206230
c
207231
~row:1
208232
~col:(max 1 (col - String.length label - 1))
209-
~style:(style_of 209)
233+
~style:(gst 209)
210234
label) ;
211235

212236
(match s.boss_intro with
@@ -219,8 +243,8 @@ let draw_game s c =
219243
let warning = C.create ~rows:2 ~cols in
220244
let msg_col = max 1 ((cols - String.length msg) / 2) in
221245
let sub_col = max 1 ((cols - String.length sub) / 2) in
222-
C.draw_text warning ~row:0 ~col:msg_col ~style:(bold_style_of fg) msg ;
223-
C.draw_text warning ~row:1 ~col:sub_col ~style:(style_of 229) sub ;
246+
C.draw_text warning ~row:0 ~col:msg_col ~style:(gbst fg) msg ;
247+
C.draw_text warning ~row:1 ~col:sub_col ~style:(gst 229) sub ;
224248
C.compose
225249
~dst:c
226250
~layers:[{C.canvas = warning; row = 3; col = 0; opaque = false}]) ;
@@ -236,7 +260,7 @@ let draw_game s c =
236260
let fg =
237261
match a.row mod 3 with 0 -> 196 | 1 -> 208 | _ -> 226
238262
in
239-
C.draw_text c ~row ~col ~style:(bold_style_of fg) ch
263+
C.draw_text c ~row ~col ~style:(gbst fg) ch
240264
| Boss ->
241265
let hp_ratio =
242266
if a.hp_max <= 0 then 0.0
@@ -247,22 +271,22 @@ let draw_game s c =
247271
else if hp_ratio > 0.33 then 214
248272
else 196
249273
in
250-
C.draw_text c ~row ~col ~style:(bold_style_of fg) boss_char
274+
C.draw_text c ~row ~col ~style:(gbst fg) boss_char
251275
end)
252276
s.aliens ;
253277

254278
List.iter
255279
(fun (b : projectile) ->
256280
let col = Float.to_int b.ppos.x + cam_x in
257281
let row = Float.to_int b.ppos.y + cam_y in
258-
C.draw_text c ~row ~col ~style:(bold_style_of b.fg) b.glyph)
282+
C.draw_text c ~row ~col ~style:(gbst b.fg) b.glyph)
259283
s.bullets ;
260284

261285
List.iter
262286
(fun (b : projectile) ->
263287
let col = Float.to_int b.ppos.x + cam_x in
264288
let row = Float.to_int b.ppos.y + cam_y in
265-
C.draw_text c ~row ~col ~style:(bold_style_of b.fg) b.glyph)
289+
C.draw_text c ~row ~col ~style:(gbst b.fg) b.glyph)
266290
s.alien_bullets ;
267291

268292
List.iter
@@ -273,7 +297,7 @@ let draw_game s c =
273297
if (s.frame + int_of_float (b.phase *. 10.0)) mod 2 = 0 then "$"
274298
else "S"
275299
in
276-
C.draw_text c ~row ~col ~style:(bold_style_of (bonus_fg b.color)) pulse)
300+
C.draw_text c ~row ~col ~style:(gbst (bonus_fg b.color)) pulse)
277301
s.bonuses ;
278302

279303
List.iter
@@ -288,15 +312,15 @@ let draw_game s c =
288312
in
289313
let col = Float.to_int e.epos.x + cam_x in
290314
let row = Float.to_int e.epos.y + cam_y in
291-
C.draw_text c ~row ~col ~style:(bold_style_of fg) ch)
315+
C.draw_text c ~row ~col ~style:(gbst fg) ch)
292316
s.explosions ;
293317

294318
List.iter
295319
(fun p ->
296320
let fg = if Anim.value p.lanim < 0.5 then 118 else 194 in
297321
let col = Float.to_int p.lpos.x - 3 + cam_x in
298322
let row = Float.to_int p.lpos.y + cam_y in
299-
C.draw_text c ~row ~col ~style:(bold_style_of fg) "+1 LIFE")
323+
C.draw_text c ~row ~col ~style:(gbst fg) "+1 LIFE")
300324
s.life_popups ;
301325

302326
(match s.phase with
@@ -326,26 +350,26 @@ let draw_game s c =
326350
c
327351
~row:ship_row
328352
~col:left_col
329-
~style:(bold_style_of pulse_fg)
353+
~style:(gbst pulse_fg)
330354
aura ;
331355
C.draw_text
332356
c
333357
~row:ship_row
334358
~col:right_col
335-
~style:(bold_style_of pulse_fg)
359+
~style:(gbst pulse_fg)
336360
aura ;
337361
if ship_row > 1 then
338362
C.draw_text
339363
c
340364
~row:(ship_row - 1)
341365
~col:(ship_col + half)
342-
~style:(style_of pulse_fg)
366+
~style:(gst pulse_fg)
343367
"^") ;
344368
C.draw_text
345369
c
346370
~row:ship_row
347371
~col:ship_col
348-
~style:(bold_style_of ship_fg)
372+
~style:(gbst ship_fg)
349373
sprite) ;
350374

351375
let hint =
@@ -369,7 +393,7 @@ let draw_game s c =
369393
~width:hud_w
370394
~height:hud_h
371395
~border:Rounded
372-
~style:(style_of 250) ;
396+
~style:(style_of ~bg:236 250) ;
373397
C.fill_rect
374398
hud
375399
~row:1
@@ -378,13 +402,18 @@ let draw_game s c =
378402
~height:(hud_h - 2)
379403
~char:" "
380404
~style:{C.default_style with bg = 236} ;
381-
C.draw_text hud ~row:1 ~col:2 ~style:(bold_style_of weapon_fg) hud_main ;
382-
C.draw_text hud ~row:2 ~col:2 ~style:(style_of 252) hud_sub ;
405+
C.draw_text
406+
hud
407+
~row:1
408+
~col:2
409+
~style:(bold_style_of ~bg:236 weapon_fg)
410+
hud_main ;
411+
C.draw_text hud ~row:2 ~col:2 ~style:(style_of ~bg:236 252) hud_sub ;
383412
C.compose
384413
~dst:c
385414
~layers:[{C.canvas = hud; row = hud_row; col = hud_col; opaque = true}]
386415
end ;
387416

388417
let hint_col = max 1 ((cols - String.length hint) / 2) in
389-
C.draw_text c ~row:(rows - 1) ~col:hint_col ~style:(style_of 240) hint
418+
C.draw_text c ~row:(rows - 1) ~col:hint_col ~style:(gst 240) hint
390419
end

miaou-core.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "0.4.1"
3+
version: "0.4.2"
44
synopsis: "Miaou core/widgets (no drivers, no SDL)"
55
maintainer: ["Nomadic Labs <contact@nomadic-labs.com>"]
66
authors: ["Nomadic Labs <contact@nomadic-labs.com>"]

miaou-driver-matrix.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "0.4.1"
3+
version: "0.4.2"
44
synopsis: "Miaou high-performance terminal driver with diff rendering"
55
maintainer: ["Nomadic Labs <contact@nomadic-labs.com>"]
66
authors: ["Nomadic Labs <contact@nomadic-labs.com>"]

miaou-driver-sdl.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "0.4.1"
3+
version: "0.4.2"
44
synopsis: "Miaou SDL driver"
55
maintainer: ["Nomadic Labs <contact@nomadic-labs.com>"]
66
authors: ["Nomadic Labs <contact@nomadic-labs.com>"]

miaou-driver-term.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "0.4.1"
3+
version: "0.4.2"
44
synopsis: "Miaou terminal driver"
55
maintainer: ["Nomadic Labs <contact@nomadic-labs.com>"]
66
authors: ["Nomadic Labs <contact@nomadic-labs.com>"]

miaou-driver-web.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "0.4.1"
3+
version: "0.4.2"
44
synopsis: "Miaou web driver (xterm.js over WebSocket)"
55
maintainer: ["Nomadic Labs <contact@nomadic-labs.com>"]
66
authors: ["Nomadic Labs <contact@nomadic-labs.com>"]

miaou-runner.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "0.4.1"
3+
version: "0.4.2"
44
synopsis: "Miaou runner with backend selection"
55
maintainer: ["Nomadic Labs <contact@nomadic-labs.com>"]
66
authors: ["Nomadic Labs <contact@nomadic-labs.com>"]

miaou-tui.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "0.4.1"
3+
version: "0.4.2"
44
synopsis: "Miaou TUI framework - terminal only (no SDL)"
55
maintainer: ["Nomadic Labs <contact@nomadic-labs.com>"]
66
authors: ["Nomadic Labs <contact@nomadic-labs.com>"]

0 commit comments

Comments
 (0)