Subject: bug#71343: 30.0.50; TTY frame doesn't automatically redisplay itself after having closed another frame

From: Daniel Clemente <[email-masked]>
Date: Sun, 16 Jun 2024 05:40:12 +0000

I have a new discovery (it's about „initial frame“), you can skip to
the gdb info below (line 16876).

> > Random ideas, without knowing much about terminals.
> > - Can't an X terminal detect „I've been given X focus“ and pass this
> > signal to the program running inside it?
>
> You assume that it will be a terminal belonging to Emacs that will get
> focus?  That is not given.
>

I was thinking that maybe X always does this (X notifies the terminal
emulator, which notifies the program running inside it, whatever
program it is).
I brainstorm ideas without X knowledge; please ignore the bad ones.
But I think „window focus“ isn't the main issue here; more on this below.

> > - What if, when closing a TTY frame in emacsclient, all other frames
> > are redisplayed
>
> Why?

This one is a bad idea, or a last resort solution, in case there's no
other way to detect that some frames (or which ones) may need to be
refreshed.

>
> > - What if, when resizing the frame (something which Emacs detects),
> > Emacs knows/detects that a frame was closed, and decides not to delay
> > the redisplay?
>
> Redisplay of which frame?  Emacs only redisplays a frame if some
> change in buffer text justifies that.

I propose it should repaint/redisplay the frame whose size changed.
Maybe I'm not using the right word, „redisplay“. What I mean is that
it should refresh its contents („frame redraw“ I think it's called) to
use the new size. For instance if I make the frame wider, now there's
more space, more words fit in each line, the minibuffer needs to be
extended to the right. Even if the buffer text didn't change.
I have 2 frames, kill the 2nd one, and resize the 1st one. Right now,
Emacs is indeed running adjust_frame_size, and it decides not to
repaint the contents; instead it leaves black areas. (Just in this
case after having closed a frame). This does seem like a bug, because
any type of frame size change should trigger a refresh to use the new
size.

Don't we know which frame is the one being resized? It should be f in
adjust_frame_size. It realizes that e.g. it's being resized from 72 to
61 columns.

(gdb) info locals
w = 0x7fffffffc000
text_area_y = -16112
text_area_x = -16288
text_area_width = -16128
text_area_height = 1102416563
unit_width = 1
unit_height = 1
old_native_width = 72
old_native_height = 50
new_native_width = 61
new_native_height = 50
min_inner_width = 2
min_inner_height = 4
r = 0x62100022d130
old_inner_width = 72
old_inner_height = 49
new_inner_width = 61
new_inner_height = 49
old_text_cols = 72
old_text_lines = 49
new_text_cols = 61
new_text_lines = 49
old_text_width = 72
old_text_height = 49
inhibit_horizontal = true
inhibit_vertical = true
frame = XIL(0x621000181105)

And yet it doesn't redraw.
I'm still learning the internals to be able to understand what
happens. resize_frame_windows does its job. pool_changed_p (in
adjust_frame_glyphs_for_frame_redisplay) is true. display_completed is
true. FRAME_GARBAGED_P (f) is true. Later in do_pending_window_change
delayed_size_change is false. When later I press a key,
clear_garbaged_frames is run (invoked by redisplay_internal)

In normal conditions, resizing a frame calls: adjust_frame size, then
redisplay_internal, then clear_garbaged_frames, then redraw_frame

But when I reproduce this bug (i.e. just after closing a frame),
resizing a frame calls: adjust_frame_size, then redisplay_internal. It
doesn't call the other two.
(Only later when I press a key, it calls redisplay_internal, then
clear_garbaged_frames, then redraw_frame)
In this scenario, why doesn't redisplay_internal reach the point where
it calls clear_garbaged_frames?
Because this happens:
Breakpoint 5, redisplay_internal () at xdisp.c:16831
16831      struct window *w = XWINDOW (selected_window);
(gdb) n
16835      bool must_finish = false, match_p;
(gdb)
16839      bool polling_stopped_here = false;
(gdb)
16846      int hscroll_retries = 0;
(gdb)
16854      int garbaged_frame_retries = 0;
(gdb)
16862      bool update_miniwindow_p = false;
(gdb)
16864      redisplay_trace ("redisplay_internal %d\n", redisplaying_p);
(gdb)
16870      if (redisplaying_p)
(gdb)
16876      if ((FRAME_INITIAL_P (SELECTED_FRAME ())
(gdb)
16877           && redisplay_skip_initial_frame)
(gdb)
16879        return;
(gdb)

So, apparently it thinks that my frame is initial.

But from a first view, that doesn't look right. The frame I'm resizing
isn't initial.
It even has a glyph matrix, 2 windows, …
If I open 4 frames, close the 4th one and resize the 3rd one, it sees
it as initial.

This part of the code is new to me. I'll research it more but I wanted
to send this information now.

I have also seen that some of my frames have f->terminal->name =
"initial_terminal", I don't know if that's related or bad.

Some more data about the looks-initial frame:

(gdb) p *SELECTED_FRAME()
$102 = {
  header = {
    size = 4611686018595348501
  },
  name = XIL(0x555557e78944),
  icon_name = XIL(0),
  title = XIL(0),
  last_mouse_device = XIL(0),
  focus_frame = XIL(0),
  root_window = XIL(0x621000004125),
  selected_window = XIL(0x621000004125),
  old_selected_window = XIL(0x621000004125),
  minibuffer_window = XIL(0x6210000043bd),
  param_alist = XIL(0x7ffff18bc633),
  scroll_bars = XIL(0),
  condemned_scroll_bars = XIL(0),
  menu_bar_items = XIL(0x6210000ca905),
  face_hash_table = XIL(0x621000004655),
  menu_bar_vector = XIL(0),
  buffer_predicate = XIL(0),
  buffer_list = XIL(0x7ffff00cbe23),
  buried_buffer_list = XIL(0),
  tool_bar_position = XIL(0xfab0),
  tab_bar_items = XIL(0),
  tool_bar_items = XIL(0),
  face_cache = 0x604000000ad0,
  last_tab_bar_item = 0,
  menu_bar_items_used = 0,
  current_pool = 0x603000002fe0,
  desired_pool = 0x603000002fb0,
  desired_matrix = 0x616000001280,
  current_matrix = 0x616000001580,
  glyphs_initialized_p = true,
  resized_p = false,
  default_face_done_p = false,
  already_hscrolled_p = false,
  updated_p = false,
  fonts_changed = false,
  cursor_type_changed = false,
  redisplay = true,
  visible = 1,
  iconified = false,
  garbaged = false,
  wants_modeline = true,
  auto_raise = false,
  auto_lower = false,
  no_split = false,
  explicit_name = false,
  window_change = false,
  window_state_change = false,
  mouse_moved = false,
  pointer_invisible = false,
  frozen_window_starts = false,
  output_method = output_initial,
  can_set_window_size = true,
  after_make_frame = true,
  tab_bar_redisplayed = false,
  tab_bar_resized = false,
--Type <RET> for more, q to quit, c to continue without paging--
  tool_bar_redisplayed = false,
  tool_bar_resized = false,
  inhibit_horizontal_resize = false,
  inhibit_vertical_resize = false,
  face_change = true,
  inhibit_clear_image_cache = false,
  new_size_p = false,
  was_invisible = false,
  select_mini_window_flag = false,
  change_stamp = 14,
  number_of_windows = 2,
  tab_bar_lines = 0,
  tab_bar_height = 0,
  n_tab_bar_rows = 0,
  n_tab_bar_items = 0,
  tool_bar_lines = 0,
  tool_bar_height = 0,
  n_tool_bar_rows = 0,
  n_tool_bar_items = 0,
  decode_mode_spec_buffer = 0x613000002f80 '\276' <repeats 200 times>...,
  insert_line_cost = 0x0,
  delete_line_cost = 0x0,
  insert_n_lines_cost = 0x0,
  delete_n_lines_cost = 0x0,
  text_cols = 80,
  text_lines = 24,
  text_width = 80,
  text_height = 24,
  total_cols = 80,
  total_lines = 25,
  pixel_width = 80,
  pixel_height = 25,
  new_width = -1,
  new_height = -1,
  left_pos = 0,
  top_pos = 0,
  win_gravity = 0,
  size_hint_flags = 0,
  border_width = 0,
  child_frame_border_width = 0,
  internal_border_width = 0,
  right_divider_width = 0,
  bottom_divider_width = 0,
  left_fringe_width = 0,
  right_fringe_width = 0,
  fringe_cols = 0,
  menu_bar_lines = 1,
  menu_bar_height = 1,
  column_width = 1,
  line_height = 1,
  terminal = 0x621000003d00,
  output_data = {
    tty = 0x0,
    x = 0x0,
    w32 = 0x0,
    ns = 0x0,
    pgtk = 0x0,
    haiku = 0x0,
--Type <RET> for more, q to quit, c to continue without paging--
    android = 0x0
  },
  font_driver_list = 0x0,
  desired_cursor = FILLED_BOX_CURSOR,
  cursor_width = 0,
  blink_off_cursor = FILLED_BOX_CURSOR,
  blink_off_cursor_width = 0,
  config_scroll_bar_width = 0,
  config_scroll_bar_cols = 0,
  config_scroll_bar_height = 0,
  config_scroll_bar_lines = 0,
  cost_calculation_baud_rate = 0,
  alpha = {0, 0},
  alpha_background = 0,
  gamma = 0,
  extra_line_spacing = 0,
  background_pixel = 18446744073709551613,
  foreground_pixel = 18446744073709551614
}
(gdb) p SELECTED_FRAME()->output_data
$103 = {
  tty = 0x0,
  x = 0x0,
  w32 = 0x0,
  ns = 0x0,
  pgtk = 0x0,
  haiku = 0x0,
  android = 0x0
}
(gdb) p Vframe_list
$104 = XIL(0x7ffff18f6c63)
(gdb) xlist
$105 = 0x6210000175bd
Lisp_Vectorlike
PVEC_FRAME
$106 = (struct frame *) 0x6210000175b8
"F2"
No symbol "PVEC_TS_QUERY" in current context.
(gdb)

If I run (set redisplay_skip_initial_frame nil), then **everything
works as expected**. It redraws well when I resize the terminal. No
black areas visible.
But I want the root issue; I still want to know why it's ¿wrongly?
classified as initial. And then fix that.

So I think this issue (71343) isn't about X focus (we're dealing with
TTYs anyway); it's about how to react to frame size changes. And right
now it's about why it unexpectedly seems a frame as initial frame.

>
> > It's ok if it can't be fixed. I'm surprised that others didn't have
> > this issue; but maybe not many are running TTY emacs (no X) inside an
> > X window.:
>
> It is a rare and not very interesting situation: users aren't expected
> to kill terminals, they are expected to "C-x #" to close Emacs
> displays.
>

By the way, C-x # isn't enough to close any Emacs frame. If I have
opened it by running   urxvtcd -e 'emacsclient' '-nw', then later C-x
# doesn't close it, it just says „No server buffers remain to edit“.
But C-x C-c does close it.

The non-repainting issues described above also happen if I close
emacsclient gracefully by C-x C-c. No killing (SIGHUP, SIGKILL, …) is
needed.

Anyway, it seems the problem may be not in the closing, but in the
resizing+redisplaying of the frame that's left. And in the
initial-frame thing, which I'll keep tracing.