Bogdanp / racket-gui-easy

Declarative GUIs in Racket.
https://docs.racket-lang.org/gui-easy/index.html
134 stars 18 forks source link

Canvas: `ctx: undefined; cannot use field before initialization` #40

Closed benknoble closed 1 year ago

benknoble commented 1 year ago

I haven't found a small or deterministic reproduction for this yet, but you should be able to follow a few steps and see the issue.

First, drop the following in ring1.rkt:

#lang frosthaven-manager/aoe

 x x
x x x
 x x

Then, drop the following in bestiary.rkt:

#lang frosthaven-manager/bestiary

begin-monster "Ancient Artillery" ("Ancient Artillery")
  [0  normal  [HP  4]   [Move  0]  [Attack  2]  [Immunities  {"Muddle"}]]
  [0  elite   [HP  7]   [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [1  normal  [HP  6]   [Move  0]  [Attack  2]  [Immunities  {"Muddle"}]]
  [1  elite   [HP  9]   [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [2  normal  [HP  8]   [Move  0]  [Attack  2]  [Immunities  {"Muddle"}]]
  [2  elite   [HP  12]  [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [3  normal  [HP  9]   [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [3  elite   [HP  14]  [Move  0]  [Attack  4]  [Immunities  {"Muddle"}]]
  [4  normal  [HP  12]  [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [4  elite   [HP  16]  [Move  0]  [Attack  5]  [Immunities  {"Muddle"}]]
  [5  normal  [HP  15]  [Move  0]  [Attack  3]  [Immunities  {"Muddle"}]]
  [5  elite   [HP  21]  [Move  0]  [Attack  5]  [Immunities  {"Muddle"}]]
  [6  normal  [HP  18]  [Move  0]  [Attack  4]  [Immunities  {"Muddle"}]]
  [6  elite   [HP  26]  [Move  0]  [Attack  6]  [Immunities  {"Muddle"}]]
  [7  normal  [HP  21]  [Move  0]  [Attack  5]  [Immunities  {"Muddle"}]]
  [7  elite   [HP  34]  [Move  0]  [Attack  7]  [Immunities  {"Muddle"}]]
end-monster

begin-ability-deck "Ancient Artillery"
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
  ["Massive Blast" 57 {"Push 1 all adjacent" "Attack -1, Range 3, aoe(ring1.rkt)"}]
end-ability-deck

Now, if you install the Frosthaven Manager and run it connected to a terminal window (to see error messages), you should be able to

  1. Skip all the inputs until you get to the "Monster DB" screen (click "Play" or "Next" until you can't).
  2. Click "Open Bestiary or Foes" and select the "bestiary.rkt" file from above.
  3. Click "Next".
  4. Click "Add Monster" and add a single Ancient Artillery (number and eliteness should not matter).
  5. Click "Next" and then "Draw Abilities". You should have one of the Massive Blast cards come up for Ancient Artillery; click the "AoE" button. Sometimes it triggers on the first click, sometimes not. But click and close the window until the window comes up blank, then check your error messages.

I see (for one click)

ctx: undefined;
 cannot use field before initialization
  context...:
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/view.rkt:31:4: get-context method in context-mixin
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/canvas.rkt:36:29
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/collects/ffi/unsafe/atomic.rkt:73:13
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/canvas-mixin.rkt:144:4: do-on-paint method in canvas-mixin
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:370:11: eventspace-handler-thread-proc
ctx: undefined;
 cannot use field before initialization
  context...:
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/view.rkt:31:4: get-context method in context-mixin
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/canvas.rkt:36:29
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/collects/ffi/unsafe/atomic.rkt:73:13
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/canvas-mixin.rkt:144:4: do-on-paint method in canvas-mixin
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:370:11: eventspace-handler-thread-proc
ctx: undefined;
 cannot use field before initialization
  context...:
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/view.rkt:31:4: get-context method in context-mixin
   /Users/Knoble/Library/Racket/8.8/pkgs/gui-easy-lib/gui/easy/private/view/canvas.rkt:36:29
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/collects/ffi/unsafe/atomic.rkt:73:13
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/canvas-mixin.rkt:144:4: do-on-paint method in canvas-mixin
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:435:6
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:486:32
   /Applications/Racket v8.8/collects/racket/private/more-scheme.rkt:148:2: call-with-break-parameterization
   /Applications/Racket v8.8/share/pkgs/gui-lib/mred/private/wx/common/queue.rkt:370:11: eventspace-handler-thread-proc

I can't explain the error, though, nor why it only shows up sometimes. Race condition?

Once you can reproduce the error, I recommend using File > Save Game to save the current game. Then you can reload it anytime with File > Load Game or, when launching the game through the command-line, by passing the save file as the first argument (e.g., frosthaven-manager <save-file>). Even here, sometimes it takes multiple "AoE" clicks to trigger the issue.

[Unfortunately the save files are not totally agnostic since they contain user-specific paths. If you wanted to create /Users/Knoble and paths under it for testing, I could send you a save file.]

Bogdanp commented 1 year ago

I can reproduce the issue, but when I minimize it, it stops happening. I haven't checked, but is this one of the places where you use multiple eventspaces? If so, that might be the culprit. What's happening is the paint callback is being triggered before the object finishes instantiating, and the only way I could see that happening is if the callback is being triggered on a separate eventspace.

benknoble commented 1 year ago

I will double-check, but yes I think the AoE window is in a separate eventspace. Maybe it doesn't need to be…

I need to wrap my head around this a little better: I think you are saying (you suspect) the renderer creates the widget in one eventspace, but the callback is happening in another?

Also, what do you mean by "when I minimize it, it stops happening"?

Bogdanp commented 1 year ago

I need to wrap my head around this a little better: I think you are saying (you suspect) the renderer creates the widget in one eventspace, but the callback is happening in another?

Yes, that would be the only explanation I can think of for why the Cocoa-level callback is ending up being called before the object is fully instantiated.

Also, what do you mean by "when I minimize it, it stops happening"?

I meant that when I make (what ought to be) a minimal example, it stops happening.

benknoble commented 1 year ago

I need to wrap my head around this a little better: I think you are saying (you suspect) the renderer creates the widget in one eventspace, but the callback is happening in another?

Yes, that would be the only explanation I can think of for why the Cocoa-level callback is ending up being called before the object is fully instantiated.

Hopefully I have time to take a look next week and let you know. Maybe I'm "holding it wrong" :)

Also, what do you mean by "when I minimize it, it stops happening"?

I meant that when I make (what ought to be) a minimal example, it stops happening.

Ah, I thought you meant "minimize the window" or some such; the overlap with GUI terms there was unfortunate.

benknoble commented 1 year ago

I need to wrap my head around this a little better: I think you are saying (you suspect) the renderer creates the widget in one eventspace, but the callback is happening in another? Yes, that would be the only explanation I can think of for why the Cocoa-level callback is ending up being called before the object is fully instantiated.

The code boils down to render/eventspace, which is like render but with an eventspace argument (and sets a custom parameter in the eventspace).

Essentially, I have (unimportant details commented or removed; in particular, an eventspace-generating macro expanded and some details removed)

(define (render/eventspace tree #:parent [parent #f] #:eventspace [es (current-eventspace)])
  (parameterize ([current-eventspace es])
    (define r (render tree parent))
    ;; set current-renderer in handler-thread of es
    #;(queue-callback (thunk (current-renderer r)) 'high-priority)
    r))

;; in the other file

(button "AoE" (thunk
               (define @pict (@> @base base->pict))
               (define cust (make-custodian))
               (define es (parameterize ([current-custodian cust]) (make-eventspace)))
               (render/eventspace
                #:eventspace es
                (window
                 #:title "AoE pattern"
                 (pict-canvas @pict values)))))

On second glance, the tree object (which is (window … (pict-canvas …)) is created in the current eventspace because of strict evaluation for functions like render/eventspace; but, render is called in the new eventspace es. Could this be the culprit?

I should be able to quickly adjust render/eventspace into a macro backed by a version of the current function that delays evaluation of tree until we are in the correct eventspace. Fingers crossed that breaks nothing else :)

benknoble commented 1 year ago

Update: the following diff did not solve my problem (by creating tree in the separate eventspace):

-(define (render/eventspace tree #:parent [parent #f] #:eventspace [es (current-eventspace)])
+(define (render/eventspace- tree #:parent [parent #f] #:eventspace [es (current-eventspace)])
   (parameterize ([current-eventspace es])
-    (define r (render tree parent))
+    (define r (render (tree) parent))
     ;; set current-renderer in handler-thread of es
     #;(queue-callback (thunk (current-renderer r)) 'high-priority)
     r))

+(define-syntax-parse-rule
+  (render/eventspace {~or* {~optional {~seq #:parent parent:expr}}
+                           {~optional {~seq #:eventspace es:expr}}}
+                     tree:expr)
+  (render/eventspace- (thunk tree) (~? (~@ #:parent parent)) (~? (~@ #:eventspace es))))
Bogdanp commented 1 year ago

@benknoble please let me know if https://github.com/Bogdanp/racket-gui-easy/commit/8799d4ef7a209a34e71e456b758b15f22b96426b fixes the issue on your end.

benknoble commented 1 year ago

Will do. To be honest I'd forgotten about this. I'll have to double-check if I found some workaround.

benknoble commented 1 year ago

Ok, I didn't find any commit that suggested I fixed this, but I can't reproduce the original issue. I'll raco pkg update anyway and let you know if I see this again.