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.
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, andop
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.
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.
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.
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.
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 :)
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.
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
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.
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