~jeffwilliams


#588 Panic due to unbalanced operation 2 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

OK, I've tried reproducing the issue with the latest in the main branch, and I can't reproduce it anymore. It seems like it might be fixed by some commit between v0.6.0 and HEAD, or one of the commits changed the timing enough that it's not happening anymore. I'll try a few more times, and if I can't reproduce it we can park it as unreproducible for now.

#588 Panic due to unbalanced operation 2 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

Hi Egon,

OK, I think I see what's going wrong here.

I ran the code with the race detector, and it didn't print any warnings about races. So I then changed the debugging code to also print stacktraces for all the push and pop operations instead of only the last few, and also the address of the stack on which the operation was being performed, and then triggered the issue. Based on this data, it seems like a stack can get recreated as an empty stack while there are still outstanding stackIds to be popped, and that this can happen within the same goroutine.

In this run, the unbalanced operation occurs in a stack with address 0xc0014fb688 while trying to pop the first entry:

unbalanced operation: stack 0xc0014fb688
unbalanced operation: asked to pop stack id 1 when current id is 0
unbalanced operation: pushed 33 times, popped 33 times
unbalanced operation: stack id that was not popped is:
(no entry)
unbalanced operation: stack id that is being popped is:
(no entry)
...
panic: unbalanced operation

goroutine 68 [running]:
gioui.org/internal/ops.(*stack).check(0xc0014fb688, {0x0?, 0x0?})
	/home/user/src/gio/internal/ops/ops.go:318 +0x4ad
gioui.org/internal/ops.(*stack).pop(0xc0014fb688, {0x0?, 0x0?})
	/home/user/src/gio/internal/ops/ops.go:323 +0x3b
gioui.org/internal/ops.PopMacro(...)
	/home/user/src/gio/internal/ops/ops.go:227
gioui.org/op.MacroOp.Stop({0xc0014fb630?, {0xabab39f3?, 0xc1918837?}, {0xa50f17a8?, 0x6?}})
	/home/user/src/gio/op/op.go:163 +0x88
gioui.org/app.(*Window).processEvent(0xc0014fa000, {0x1b06460?, 0xc000f2b2c0?})
	/home/user/src/gio/app/window.go:607 +0x2185
...

The relevant code is this (I numbered some of the lines for later reference):

package app

     func (w *Window) processEvent(e event.Event) bool {
       case frameEvent:
603    wrapper := &w.decorations.Ops
604    wrapper.Reset()
605    m := op.Record(wrapper)
606    offset := w.decorate(e2.FrameEvent, wrapper)
607    w.lastFrame.deco = m.Stop()

The operation that is panicking is line 607, where the MacroOp.Stop call is trying to clean up (pop) the op.Record (push) from line 605. However, it seems that it is possible for like 606 to zero the macro stack. Examining the stacktraces for earlier pop operations, I see there are two back-to-back push calls that both create the first entry in the stack, which should not be possible. Log entries for those pushes are below. In the entries, the format for the first line is:

	  fmt.Fprintf(os.Stderr, "stack %p, id %d -> %d: %s", stack, oldID, newID, op)

where for a push operation, stack is a pointer to the stack on which the push operation is being performed, oldID is the ID of the top of the stack before the push, newID is the top of the stack after, and op is set to "push".

stack 0xc0014fb688, id 0 -> 1: push  /home/user/src/gio/internal/ops/ops.go:223  gioui.org/internal/ops.PushMacro
  /home/user/src/gio/op/op.go:151  gioui.org/op.Record
  /home/user/src/gio/app/window.go:605  gioui.org/app.(*Window).processEvent  <-- Outer call to processEvent
  /home/user/src/gio/app/window.go:389  gioui.org/app.(*callbacks).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1392  gioui.org/app.(*window).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1780  gioui.org/app.(*window).draw
  /home/user/src/gio/app/os_wayland.go:553  gioui.org/app.gio_onXdgSurfaceConfigure
  _cgo_gotypes.go:3218  _cgoexp_b6fb7491930c_gio_onXdgSurfaceConfigure
  /home/user/Downloads/go-1.21.4/src/runtime/cgocall.go:329  runtime.cgocallbackg1
  /home/user/Downloads/go-1.21.4/src/runtime/cgocall.go:245  runtime.cgocallbackg
  /home/user/Downloads/go-1.21.4/src/runtime/asm_amd64.s:1035  runtime.cgocallback
  /home/user/Downloads/go-1.21.4/src/runtime/asm_amd64.s:474  runtime.systemstack_switch
  /home/user/Downloads/go-1.21.4/src/runtime/cgocall.go:175  runtime.cgocall
  _cgo_gotypes.go:2233  gioui.org/app._Cfunc_wl_display_dispatch_pending
  /home/user/src/gio/app/os_wayland.go:1484  gioui.org/app.(*wlDisplay).dispatch.func8
  /home/user/src/gio/app/os_wayland.go:1484  gioui.org/app.(*wlDisplay).dispatch
  /home/user/src/gio/app/os_wayland.go:1377  gioui.org/app.(*window).dispatch
  /home/user/src/gio/app/os_wayland.go:1399  gioui.org/app.(*window).Event
  /home/user/src/gio/app/window.go:686  gioui.org/app.(*Window).Event

stack 0xc0014fb688, id 0 -> 1: push  /home/user/src/gio/internal/ops/ops.go:223  gioui.org/internal/ops.PushMacro
  /home/user/src/gio/op/op.go:151  gioui.org/op.Record
  /home/user/src/gio/app/window.go:605  gioui.org/app.(*Window).processEvent  <-- Inner call to processEvent
  /home/user/src/gio/app/window.go:389  gioui.org/app.(*callbacks).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1392  gioui.org/app.(*window).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1780  gioui.org/app.(*window).draw
  /home/user/src/gio/app/os_wayland.go:1505  gioui.org/app.(*window).SetAnimating
  /home/user/src/gio/app/window.go:337  gioui.org/app.(*Window).updateAnimation
  /home/user/src/gio/app/window.go:640  gioui.org/app.(*Window).processEvent
  /home/user/src/gio/app/window.go:389  gioui.org/app.(*callbacks).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1392  gioui.org/app.(*window).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1105  gioui.org/app.(*window).Configure
  /home/user/src/gio/app/window.go:772  gioui.org/app.(*Window).decorate
  /home/user/src/gio/app/window.go:606  gioui.org/app.(*Window).processEvent <-- Outer call to processEvent
  /home/user/src/gio/app/window.go:389  gioui.org/app.(*callbacks).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1392  gioui.org/app.(*window).ProcessEvent
  /home/user/src/gio/app/os_wayland.go:1780  gioui.org/app.(*window).draw
  /home/user/src/gio/app/os_wayland.go:553  gioui.org/app.gio_onXdgSurfaceConfigure
  _cgo_gotypes.go:3218  _cgoexp_b6fb7491930c_gio_onXdgSurfaceConfigure
  /home/user/Downloads/go-1.21.4/src/runtime/cgocall.go:329  runtime.cgocallbackg1
  /home/user/Downloads/go-1.21.4/src/runtime/cgocall.go:245  runtime.cgocallbackg
  /home/user/Downloads/go-1.21.4/src/runtime/asm_amd64.s:1035  runtime.cgocallback
  /home/user/Downloads/go-1.21.4/src/runtime/asm_amd64.s:474  runtime.systemstack_switch
  /home/user/Downloads/go-1.21.4/src/runtime/cgocall.go:175  runtime.cgocall
  _cgo_gotypes.go:2233  gioui.org/app._Cfunc_wl_display_dispatch_pending
  /home/user/src/gio/app/os_wayland.go:1484  gioui.org/app.(*wlDisplay).dispatch.func8
  /home/user/src/gio/app/os_wayland.go:1484  gioui.org/app.(*wlDisplay).dispatch
  /home/user/src/gio/app/os_wayland.go:1377  gioui.org/app.(*window).dispatch
  /home/user/src/gio/app/os_wayland.go:1399  gioui.org/app.(*window).Event
  /home/user/src/gio/app/window.go:686  gioui.org/app.(*Window).Event

The first entry shows the first push to the stack being performed by the outermost (first) call to create the macro on line 605 (marked with an <-- arrow). The next one shows that the outermost call is still in progress, but we recursively call Window.processEvent and create the macro a second time on line 605 and again perform the first push.

Examining the code it seems that in-between the outer and inner call the macro stack gets reset. In the inner call, line 604 calls wrapper.Reset, and from code inspection that seems to zero the macro stack:

604    wrapper.Reset()

package op

func (o *Ops) Reset() {
  ops.Reset(&o.Internal)
}

package ops

func Reset(o *Ops) {
  o.macroStack = stack{}   <-- macro stack is zeroed here
  o.stacks = [_StackKind]stack{}
  // Leave references to the GC.
  for i := range o.refs {
    o.refs[i] = nil
  }
  for i := range o.stringRefs {
    o.stringRefs[i] = ""
  }
  o.data = o.data[:0]
  o.refs = o.refs[:0]
  o.stringRefs = o.stringRefs[:0]
  o.nextStateID = 0
  o.version++
}

So the outer call still retains a stack ID that is only valid in the old, deleted stack. Once the calls resolve back to the outermost processEvent call it tries to pop using its old stackID and it panics.

I haven't yet though about what the best way to solve this might be.

Let me know if you want a copy of the full log of stacktraces for investigation.

#588 Panic due to unbalanced operation 3 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

Hi Egon,

I fixed the errors reported by vet, and performed an inspection of the places in the code where I perform Push and Pop operations, but didn't see anything that really stood out as a missing Pop.

So the I added some extra debugging in the stack-related functions in internal/ops/ops.go that you pointed to and reproduced the issue. I'll share the debug code I added first, then show the results when I reproduced the issue, and my interpretation so far. Could you double-check my interpretation to see if it makes sense?

First, here is the code I added:

diff --git a/internal/ops/dbg.go b/internal/ops/dbg.go
new file mode 100644
index 00000000..61e6eaa1
--- /dev/null
+++ b/internal/ops/dbg.go
@@ -0,0 +1,103 @@
+package ops
+
+import (
+	"bytes"
+	"fmt"
+	"runtime"
+)
+
+type stackDebugger struct {
+	// map stack ids to debug entries
+	entries             map[uint32]stackDebugEntry
+	pushCount, popCount int
+}
+
+func (s *stackDebugger) record(stackId uint32) {
+	e := newStackDebugEntry()
+
+	if s.entries == nil {
+		s.entries = make(map[uint32]stackDebugEntry)
+	}
+	s.entries[stackId] = e
+
+	s.pushCount++
+}
+
+func (s *stackDebugger) forget(stackId uint32) {
+	if s.entries == nil {
+		return
+	}
+	delete(s.entries, stackId)
+	s.popCount++
+}
+
+func (s stackDebugger) stacktrace(stackId uint32) string {
+	if s.entries == nil {
+		return "(no entry)"
+	}
+
+	e, ok := s.entries[stackId]
+	if !ok {
+		return "(no entry)"
+	}
+
+	var b bytes.Buffer
+	fmt.Fprintf(&b, "stack id %d\n", stackId)
+	for _, f := range e.frames {
+		fmt.Fprintf(&b, "%s:%d  %s\n", f.file, f.line, f.function)
+	}
+	return b.String()
+}
+
+const maxStackFrames = 5
+
+type stackDebugEntry struct {
+	stackID uint32
+	data    [maxStackFrames]frame
+	// frames is a slice into data of the valid frames.
+	frames []frame
+}
+
+type frame struct {
+	function string
+	file     string
+	line     int
+}
+
+var stackDebugEntryPcs [maxStackFrames]uintptr
+
+func newStackDebugEntry() (result stackDebugEntry) {
+
+	// framesToSkip are the number of most recent of stacktrace frames to skip
+	// when recording the stacktrace. We skip:
+	//	* The call to runtime.Callers
+	//  * The call to newStackDebugEntry
+	//  * The call to stackDebugger.record
+	//  * The call to stack.push
+	framesToSkip := 4
+
+	s := stackDebugEntryPcs[:]
+	runtime.Callers(framesToSkip, s)
+
+	frames := runtime.CallersFrames(s)
+
+	i := 0
+	for {
+		f, more := frames.Next()
+
+		result.data[i] = frame{
+			function: f.Function,
+			file:     f.File,
+			line:     f.Line,
+		}
+
+		i++
+
+		if !more {
+			break
+		}
+	}
+
+	result.frames = result.data[:i]
+	return
+}
diff --git a/internal/ops/ops.go b/internal/ops/ops.go
index e0cae3a7..a82e7768 100644
--- a/internal/ops/ops.go
+++ b/internal/ops/ops.go
@@ -4,8 +4,10 @@ package ops
 
 import (
 	"encoding/binary"
+	"fmt"
 	"image"
 	"math"
+	"os"
 
 	"gioui.org/f32"
 	"gioui.org/internal/byteslice"
@@ -97,6 +99,7 @@ type StateOp struct {
 type stack struct {
 	currentID uint32
 	nextID    uint32
+	dbg       stackDebugger
 }
 
 type StackKind uint8
@@ -299,17 +302,23 @@ func (s *stack) push() StackID {
 		prev: s.currentID,
 	}
 	s.currentID = s.nextID
+	s.dbg.record(s.currentID)
 	return sid
 }
 
 func (s *stack) check(sid StackID) {
 	if s.currentID != sid.id {
+		fmt.Fprintf(os.Stderr, "unbalanced operation: asked to pop stack id %d when current id is %d\n", sid.id, s.currentID)
+		fmt.Fprintf(os.Stderr, "unbalanced operation: pushed %d times, popped %d times\n", s.dbg.pushCount, s.dbg.popCount)
+		fmt.Fprintf(os.Stderr, "unbalanced operation: stack id that was not popped is:\n%s\n", s.dbg.stacktrace(s.currentID))
+		fmt.Fprintf(os.Stderr, "unbalanced operation: stack id that is being popped is:\n%s\n", s.dbg.stacktrace(sid.id))
 		panic("unbalanced operation")
 	}
 }
 
 func (s *stack) pop(sid StackID) {
 	s.check(sid)
+	s.dbg.forget(sid.id)
 	s.currentID = sid.prev
 }

Basically, I record a partial stacktrace when a push operation is performed with a given id. If there is an imbalance, I print out the stacktrace of the push corresponding to the invalid pop's stack id, and the stacktrace of the push corresponding to the current stack's id. Also I print out the total number of push and pop operations so far.

Here is the result for the issue reproduction:

unbalanced operation: asked to pop stack id 1 when current id is 0
unbalanced operation: pushed 33 times, popped 33 times
unbalanced operation: stack id that was not popped is:
(no entry)
unbalanced operation: stack id that is being popped is:
(no entry)
panic: unbalanced operation

goroutine 16 [running]:
gioui.org/internal/ops.(*stack).check(0xc0000c7918, {0x0?, 0x0?})
        /home/user/src/gio/internal/ops/ops.go:315 +0x1f9
gioui.org/internal/ops.(*stack).pop(0xc0000c7918, {0x0?, 0x0?})
        /home/user/src/gio/internal/ops/ops.go:320 +0x25
gioui.org/internal/ops.PopMacro(...)
        /home/user/src/gio/internal/ops/ops.go:227
gioui.org/op.MacroOp.Stop({0xc0000c78c0?, {0x55ef555f?, 0xc190dcd7?}, {0xe439fcb0?, 0x8?}})
        /home/user/src/gio/op/op.go:163 +0x39
gioui.org/app.(*Window).processEvent(0xc0000c7500, {0x12d48a0?, 0xc001741260?})
        /home/user/src/gio/app/window.go:607 +0x1105
gioui.org/app.(*callbacks).ProcessEvent(...)
        /home/user/src/gio/app/window.go:389
gioui.org/app.(*window).ProcessEvent(...)
        /home/user/src/gio/app/os_wayland.go:1392
gioui.org/app.(*window).draw(0xc00167d800, 0x1)
        /home/user/src/gio/app/os_wayland.go:1780 +0x29d
gioui.org/app.gio_onXdgSurfaceConfigure(0x86731d?, 0x4124e5?, 0x161b)
        /home/user/src/gio/app/os_wayland.go:553 +0x7b
gioui.org/app._Cfunc_wl_display_dispatch_pending(0x21eed60)
        _cgo_gotypes.go:2233 +0x47
gioui.org/app.(*wlDisplay).dispatch.func8(0xc0021165b8?)
        /home/user/src/gio/app/os_wayland.go:1484 +0x3c
gioui.org/app.(*wlDisplay).dispatch(0xc002116500)
        /home/user/src/gio/app/os_wayland.go:1484 +0x29f
gioui.org/app.(*window).dispatch(0xc00167d800)
        /home/user/src/gio/app/os_wayland.go:1377 +0x37
gioui.org/app.(*window).Event(0xc00167d800)
        /home/user/src/gio/app/os_wayland.go:1399 +0x25
gioui.org/app.(*Window).Event(0xc0000c7500)
        /home/user/src/gio/app/window.go:686 +0x38
main.loop.func2()
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:335 +0x35
created by main.loop in goroutine 1
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:333 +0x165

For comparison, here is the output when I deliberately introduce a bug where I "forget" a pop, to demonstrate how the debug code behaves in that case:

unbalanced operation: pushed 51 times, popped 49 times
unbalanced operation: stack id that was not popped is:
stack id 5
/home/user/src/gio/internal/ops/ops.go:252  gioui.org/internal/ops.PushOp
/home/user/src/gio/op/op.go:197  gioui.org/op.TransformOp.Push
/home/user/src/anvil-suite/anvil/src/anvil/editor.go:407  main.(*editorLayouter).offset
/home/user/src/anvil-suite/anvil/src/anvil/editor.go:390  main.(*editorLayouter).layout
/home/user/src/anvil-suite/anvil/src/anvil/editor.go:348  main.(*Editor).Layout

unbalanced operation: stack id that is being popped is:
stack id 4
/home/user/src/gio/internal/ops/ops.go:252  gioui.org/internal/ops.PushOp
/home/user/src/gio/op/op.go:197  gioui.org/op.TransformOp.Push
/home/user/src/anvil-suite/anvil/src/anvil/editor.go:407  main.(*editorLayouter).offset
/home/user/src/anvil-suite/anvil/src/anvil/editor.go:385  main.(*editorLayouter).layout
/home/user/src/anvil-suite/anvil/src/anvil/editor.go:348  main.(*Editor).Layout

panic: unbalanced operation [recovered]
        panic: unbalanced operation

goroutine 1 [running]:
main.loop.func1()
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:315 +0x4b
panic({0xe4e440?, 0x12d2830?})
        /home/user/Downloads/go-1.21.4/src/runtime/panic.go:914 +0x21f
gioui.org/internal/ops.(*stack).check(0xc0001a8a98, {0x258?, 0x0?})
        /home/user/src/gio/internal/ops/ops.go:315 +0x1f9
gioui.org/internal/ops.(*stack).pop(0xc0001a8a98, {0x0?, 0x0?})
        /home/user/src/gio/internal/ops/ops.go:320 +0x25
gioui.org/internal/ops.PopOp(...)
        /home/user/src/gio/internal/ops/ops.go:259
gioui.org/op.TransformStack.Pop({{0x17352e0?, 0xc0?}, 0x258?, 0xc0001a8a00?})
        /home/user/src/gio/op/op.go:225 +0x45
main.(*editorLayouter).layout(0xc0017352e0, {{{0x320, 0x258}, {0x320, 0x258}}, {0x3f800000, 0x3f800000}, {0xc190dcbb6fcc06df, 0xce80391, 0x194b5c0}, ...})
        /home/user/src/anvil-suite/anvil/src/anvil/editor.go:401 +0x18d
main.(*Editor).Layout(0xc001734d00, {{{0x320, 0x258}, {0x320, 0x258}}, {0x3f800000, 0x3f800000}, {0xc190dcbb6fcc06df, 0xce80391, 0x194b5c0}, ...})
        /home/user/src/anvil-suite/anvil/src/anvil/editor.go:348 +0x9b
main.layoutWidgets(...)
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:438
main.handleEvent({0x12d3860?, 0xc001e1a5a0?})
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:377 +0x378
main.loop(0xc001770000, 0x2?)
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:347 +0x2bf
main.main()
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:69 +0x405

So, the way I see this is that in the reported issue reproduction, it seems like Window.processEvent is trying to pop id 1 from an empty stack. If I read the code right, if the current id of a stack is 0 (which it is in this case) then that stack is empty, since the very first push operation will have ID 1. Furthermore, the count of pushes and pops is equal.

So it seems like Window.processEvent is trying to pop the very first push (ID 1) from an empty stack. Since there were more than zero pushes, this implies that some other code had successfully popped ID 1. But in a non-concurrent execution, how could that code have gotten ID 1 when it had already been used?

The code in Window.processEvent is pretty simple, and the pop (performed by MacroOp.Stop) is very close to the Push (op.Record), so I don't see anything obviously wrong here:

    m := op.Record(wrapper)
    offset := w.decorate(e2.FrameEvent, wrapper)
    w.lastFrame.deco = m.Stop()

Any ideas? I'll also try and record and display the last pop operation in the stack debugger to see if it provides a hint as to what might have emptied the stack.

#588 Panic due to unbalanced operation 3 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

Hi Egon,

Thanks for the feedback and suggestions! I'll investigate the code with those in mind, and run go vet.

#588 Panic due to unbalanced operation 3 months ago

Ticket created by ~jeffwilliams on ~eliasnaur/gio

Hi folks,

I'm seeing a panic that occurs sporadically in GIO v0.6.0 when a window is resized. It occurs more often when a window is resized (made smaller and larger) in small increments, though sometimes also occurs in larger resizes.

The panic stacktrace is below. Any ideas of how to troubleshoot it? If need be, I could add fmt.Printf statements to GIO at appropriate places to help get diagnostic information.

panic: unbalanced operation

goroutine 50 [running]:
gioui.org/internal/ops.(*stack).check(...)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/internal/ops/ops.go:307
gioui.org/internal/ops.(*stack).pop(...)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/internal/ops/ops.go:312
gioui.org/internal/ops.PopMacro(...)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/internal/ops/ops.go:224
gioui.org/op.MacroOp.Stop({0xc000b2fb48?, {0xa26c1d0d?, 0xc18fda6e?}, {0xe2e761f1?, 0x6?}})
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/op/op.go:163 +0xc5
gioui.org/app.(*Window).processEvent(0xc000b2f800, {0x12d0620?, 0xc00200a900?})
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/window.go:607 +0x1098
gioui.org/app.(*callbacks).ProcessEvent(...)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/window.go:389
gioui.org/app.(*window).ProcessEvent(...)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/os_wayland.go:1392
gioui.org/app.(*window).draw(0xc000d3f800, 0x1)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/os_wayland.go:1780 +0x29d
gioui.org/app.gio_onXdgSurfaceConfigure(0x86713d?, 0x4124e5?, 0x7a1)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/os_wayland.go:553 +0x7b
gioui.org/app._Cfunc_wl_display_dispatch_pending(0x24e9c00)
        _cgo_gotypes.go:2233 +0x47
gioui.org/app.(*wlDisplay).dispatch.func8(0xc00211e338?)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/os_wayland.go:1484 +0x3c
gioui.org/app.(*wlDisplay).dispatch(0xc00211e280)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/os_wayland.go:1484 +0x29f
gioui.org/app.(*window).dispatch(0xc000d3f800)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/os_wayland.go:1377 +0x37
gioui.org/app.(*window).Event(0xc000d3f800)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/os_wayland.go:1399 +0x25
gioui.org/app.(*Window).Event(0xc000b2f800)
        /home/user/gows/pkg/mod/gioui.org@v0.6.0/app/window.go:686 +0x38
main.loop.func2()
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:333 +0x35
created by main.loop in goroutine 1
        /home/user/src/anvil-suite/anvil/src/anvil/main.go:331 +0x165

This is observed when running a Linux binary under the Windows Subsystem for Linux 2 (WSL2) on Windows 10. I haven't observed it in native Windows executables. It could be there is a timing issue that is exacerbated by the emulation provided by the subsystem.

#551 Ascent and Descent of shaped Tab character differs from alphanumeric characters 8 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

Yeah, I think you're right. Since a tab is not really a visible character in the first place, and since most applications would choose their own advance for it, which might even be different depending on where it falls in the line of text, the font metrics for it are not really meaningful. The same probably applies to other control characters like space, vertical tab, carriage return, newline, escape, etc.

In my case, I ran into this as part of typesetting because I was "expanding" tabs in the text. Logically, I'd break up a line of text into pieces which were either (1) contiguous strings that don't contain tab, or (2) a tab. Then I'd adjust the Advance of the tab Glyphs based on how much whitespace I wanted each tab to take. Then I'd layout the line, and since I needed the height of the line I'd take the minimum height out of all the pieces, so I'd end up getting a smaller height for those lines with tabs.

I just changed it to use the Ascent and Descent of the 'X' glyph instead. Most fonts should have that glyph, and I'm fine with assuming all glyphs have the same height in the font :)

#551 Ascent and Descent of shaped Tab character differs from alphanumeric characters 8 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

That seems to be it! When I added text.NoSystemFonts(), the output for the tab character matches:

$ go build && ./font-test  
Loading font from file InputSansCondensed-ExtraLight.ttf
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{97}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc00025f2f0), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x6c61746e, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:650, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:465, Height:-579, XBearing:101, YBearing:568, XAdvance:650, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xd7, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-11, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:650, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:465, Height:-579, XBearing:101, YBearing:568, XAdvance:650, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xd7, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-11, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x400000000d7, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:650, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-568}, Max:fixed.Point26_6{X:566, Y:11}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:650, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc00025f2f0), truncator:false}}, visualOrder:[]int{0}, width:650, ascent:950, descent:279, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-950}, Max:fixed.Point26_6{X:734, Y:279}}, direction:0x0, runeCount:1, yOffset:15}}
Ascent of rune a (97): 950  Descent: 279
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{98}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc00025f2f0), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x6c61746e, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:371, Height:-754, XBearing:101, YBearing:745, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe2, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:745, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:371, Height:-754, XBearing:101, YBearing:745, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe2, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:745, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x400000000e2, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:573, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-745}, Max:fixed.Point26_6{X:472, Y:9}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:573, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc00025f2f0), truncator:false}}, visualOrder:[]int{0}, width:573, ascent:950, descent:279, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-950}, Max:fixed.Point26_6{X:674, Y:279}}, direction:0x0, runeCount:1, yOffset:15}}
Ascent of rune b (98): 950  Descent: 279
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{99}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc00025f2f0), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x6c61746e, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:381, Height:-577, XBearing:101, YBearing:568, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe3, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:381, Height:-577, XBearing:101, YBearing:568, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe3, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x400000000e3, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:573, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-568}, Max:fixed.Point26_6{X:482, Y:9}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:573, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc00025f2f0), truncator:false}}, visualOrder:[]int{0}, width:573, ascent:950, descent:279, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-950}, Max:fixed.Point26_6{X:664, Y:279}}, direction:0x0, runeCount:1, yOffset:15}}
Ascent of rune c (99): 950  Descent: 279
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{9}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc00025f2f0), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x7a797979, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:650, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:557, Height:-1024, XBearing:47, YBearing:791, XAdvance:650, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0x0, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:791, Descent:-233, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:650, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:557, Height:-1024, XBearing:47, YBearing:791, XAdvance:650, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0x0, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:791, Descent:-233, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc00025f2f0)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x40000000000, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:650, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:47, Y:-791}, Max:fixed.Point26_6{X:604, Y:233}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:650, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc00025f2f0), truncator:false}}, visualOrder:[]int{0}, width:650, ascent:950, descent:279, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:47, Y:-950}, Max:fixed.Point26_6{X:696, Y:279}}, direction:0x0, runeCount:1, yOffset:15}}
Ascent of rune 	 (9): 950  Descent: 279

I think the font doesn't include a rune for the tab character, if I am interpreting the output of fc-query correctly. That could be related:

$ fc-query --format='%{charset}\n' InputSansCondensed-ExtraLight.ttf 
20-7e a0-17f 192 1fa-1ff 218-21b 237 2bb 2c6-2c7 2c9 2d8-2dd 300-304 306-308 30a-30c 312 326-328 384-38a 38c 38e-3a1 3a3-3ce 400-45f 490-491 1e80-1e85 1ef2-1ef3 2000-2005 2007 2009-200b 2010-2011 2013-2015 2017-201e 2020-2022 2026 2030 2032-2033 2039-203a 203c 203e 2044 2070 2074-2079 207f-2089 20a3-20a4 20a7 20ac 2105 2113 2116-2117 2122 2126 212e 215b-215e 2190-2195 21a8 2202 2206 220f 2211-2212 2215 2219-221a 221e-221f 2229 222b 2248 2260-2261 2264-2265 2310 2321 23af 2500-25a1 25aa-25ab 25b2 25ba 25bc 25c4 25ca-25cb 25cf 25e6 2619 2640 2642 2660 2663 2665-2666 266a 26a1 2713 2767 e000-e060 e0a0-e0a2 e0b0-e0b3 f6be f6c3 f8ff fb01-fb02 feff 1f41b

Looks like the lowest numbered unicode point in the font is the space glyph.

#551 Ascent and Descent of shaped Tab character differs from alphanumeric characters 8 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

Sure thing! Here is the output:

$ ./font-test 
Loading font from file InputSansCondensed-ExtraLight.ttf
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{97}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc000100270), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x6c61746e, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:650, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:465, Height:-579, XBearing:101, YBearing:568, XAdvance:650, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xd7, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-11, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc000100270)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:650, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:465, Height:-579, XBearing:101, YBearing:568, XAdvance:650, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xd7, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-11, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc000100270)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x400000000d7, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:650, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-568}, Max:fixed.Point26_6{X:566, Y:11}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:650, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc000100270), truncator:false}}, visualOrder:[]int{0}, width:650, ascent:950, descent:279, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-950}, Max:fixed.Point26_6{X:734, Y:279}}, direction:0x0, runeCount:1, yOffset:15}}
Ascent of rune a (97): 950  Descent: 279
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{98}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc000100270), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x6c61746e, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:371, Height:-754, XBearing:101, YBearing:745, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe2, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:745, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc000100270)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:371, Height:-754, XBearing:101, YBearing:745, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe2, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:745, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc000100270)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x400000000e2, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:573, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-745}, Max:fixed.Point26_6{X:472, Y:9}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:573, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc000100270), truncator:false}}, visualOrder:[]int{0}, width:573, ascent:950, descent:279, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-950}, Max:fixed.Point26_6{X:674, Y:279}}, direction:0x0, runeCount:1, yOffset:15}}
Ascent of rune b (98): 950  Descent: 279
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{99}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc000100270), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x6c61746e, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:381, Height:-577, XBearing:101, YBearing:568, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe3, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc000100270)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:573, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:381, Height:-577, XBearing:101, YBearing:568, XAdvance:573, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0xe3, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:950, Descent:-279, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:568, Descent:-9, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc000100270)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x400000000e3, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:573, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-568}, Max:fixed.Point26_6{X:482, Y:9}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:573, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc000100270), truncator:false}}, visualOrder:[]int{0}, width:573, ascent:950, descent:279, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:101, Y:-950}, Max:fixed.Point26_6{X:664, Y:279}}, direction:0x0, runeCount:1, yOffset:15}}
Ascent of rune c (99): 950  Descent: 279
shaper inputs: []shaping.Input{shaping.Input{Text:[]int32{9}, RunStart:0, RunEnd:1, Direction:0x0, Face:(*font.Face)(0xc0007366f0), FontFeatures:[]shaping.FontFeature(nil), Size:1024, Script:0x7a797979, Language:""}}
shaper outputs: []shaping.Output{shaping.Output{Advance:229, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:0, Height:0, XBearing:0, YBearing:0, XAdvance:229, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0x1, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:1188, Descent:-328, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:0, Descent:0, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc0007366f0)}}
wrapped lines: []shaping.Line{shaping.Line{shaping.Output{Advance:229, Size:1024, Glyphs:[]shaping.Glyph{shaping.Glyph{Width:0, Height:0, XBearing:0, YBearing:0, XAdvance:229, YAdvance:0, XOffset:0, YOffset:0, ClusterIndex:0, RuneCount:1, GlyphCount:1, GlyphID:0x1, Mask:0x80000000}}, LineBounds:shaping.Bounds{Ascent:1188, Descent:-328, Gap:0}, GlyphBounds:shaping.Bounds{Ascent:0, Descent:0, Gap:0}, Direction:0x0, Runes:shaping.Range{Offset:0, Count:1}, Face:(*font.Face)(0xc0007366f0)}}}
final gio lines: []text.line{text.line{runs:[]text.runLayout{text.runLayout{VisualPosition:0, X:0, Glyphs:[]text.glyph{text.glyph{id:0x1040000000001, clusterIndex:0, glyphCount:1, runeCount:1, xAdvance:229, yAdvance:0, xOffset:0, yOffset:0, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:0, Y:0}, Max:fixed.Point26_6{X:0, Y:0}}}}, Runes:text.Range{Count:1, Offset:0}, Advance:229, PPEM:1024, Direction:0x0, face:(*font.Face)(0xc0007366f0), truncator:false}}, visualOrder:[]int{0}, width:229, ascent:1188, descent:328, lineHeight:1228, bounds:fixed.Rectangle26_6{Min:fixed.Point26_6{X:0, Y:-1188}, Max:fixed.Point26_6{X:458, Y:328}}, direction:0x0, runeCount:1, yOffset:19}}
Ascent of rune 	 (9): 1188  Descent: 328

#551 Ascent and Descent of shaped Tab character differs from alphanumeric characters 8 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

If you end up still having trouble reproducing it, maybe it could help if you send me a diff that contains some fmt.Printfs in the relevant GIO or go-text code and I could patch it in and send the output of a run.

#551 Ascent and Descent of shaped Tab character differs from alphanumeric characters 8 months ago

Comment by ~jeffwilliams on ~eliasnaur/gio

Hmm, that is really odd. It must be some difference in my setup or environment.

I downloaded the font again from the site a couple times (there are two places on the site to download it) and I still see the same behaviour. Each time I download the font I get a slightly different file hash---it seems like the font is modified slightly when it's prepared for download, which I guess is expected:

$ sha256sum *ttf*
54d880aa90abd74f6dbab20cc982470520f5e116b577ea7df13a8df08004b82  InputSansCondensed-ExtraLight.ttf.download1
79f874382b8319fc75aee44435ea3068dd5a52d0ca8da30714fb70cc0697b7a6  InputSansCondensed-ExtraLight.ttf.download2
2b7c352a04f90f980a0ab894920ffc8036e899af5cd16fbd3acfe4bb657424a3  InputSansCondensed-ExtraLight.ttf.old
$ diff <(hexdump -C InputSansCondensed-ExtraLight.ttf.old) <(hexdump -C InputSansCondensed-ExtraLight.ttf.download1)
10c10
< 00000090  11 d5 a1 9b 00 00 01 0c  00 00 00 36 68 68 65 61  |...........6hhea|
---
> 00000090  14 89 ee 93 00 00 01 0c  00 00 00 36 68 68 65 61  |...........6hhea|
18,19c18,19
< 00000110  00 01 19 9a 6f 51 46 e5  5f 0f 3c f5 00 19 04 4c  |....oQF._.<....L|
< 00000120  00 00 00 00 cf bc a9 9b  00 00 00 00 de ea 99 69  |...............i|
---
> 00000110  00 01 19 9a 69 e8 ac f5  5f 0f 3c f5 00 19 04 4c  |....i..._.<....L|
> 00000120  00 00 00 00 cf bc a9 9b  00 00 00 00 e1 9e e6 61  |...............a|

I modified the sample program slightly so that the font file can be passed as an argument. Here is the output for the ones I downloaded and the old one I had:

$ for i in *ttf?*; do ./font-test $i; done
Loading font from file InputSansCondensed-ExtraLight.ttf.download1
Ascent of rune a (97): 950  Descent: 279
Ascent of rune b (98): 950  Descent: 279
Ascent of rune c (99): 950  Descent: 279
Ascent of rune 	 (9): 1188  Descent: 328
Loading font from file InputSansCondensed-ExtraLight.ttf.download2
Ascent of rune a (97): 950  Descent: 279
Ascent of rune b (98): 950  Descent: 279
Ascent of rune c (99): 950  Descent: 279
Ascent of rune 	 (9): 1188  Descent: 328
Loading font from file InputSansCondensed-ExtraLight.ttf.old
Ascent of rune a (97): 950  Descent: 279
Ascent of rune b (98): 950  Descent: 279
Ascent of rune c (99): 950  Descent: 279
Ascent of rune 	 (9): 1188  Descent: 328

I packaged up the sample program (including the go.mod), the gio and go-text repos checked out to the versions I ran the sample program on, and the font files I downloaded here:

http://zoom.rendermotion.ca/tmp/for-chris-gio-issue-551.tar.gz

Perhaps having the same code and go.mod for the sample program might make it reproducible. There is a readme.txt in the archive that describes what's in it.

Here is the output of go version and go env:

$ go version 
go version go1.21.4 linux/amd64

$ go env 
GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/jefwill/.cache/go-build'
GOENV='/home/jefwill/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/jefwill/gows/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/jefwill/gows'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/jefwill/Downloads/go-1.21.4'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/jefwill/Downloads/go-1.21.4/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.21.4'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/jefwill/src/anvil-tab-rendering/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1907764494=/tmp/go-build -gno-record-gcc-switches'

The go.mod and go.sum should be in that archive.

In case it's helpful here is a bit more info about the OS version I have:

$ cat /etc/debian_version
10.2
$ uname -a 
Linux debian 4.19.0-6-amd64 # 1 SMP Debian 4.19.67-2+deb10u2 (2019-11-11) x86_64 GNU/Linux