vydd / sketch

A Common Lisp framework for the creation of electronic art, visual design, game prototyping, game making, computer graphics, exploration of human-computer interaction, and more.
MIT License
1.4k stars 67 forks source link

Delivering application based on sketch #58

Open Gleefre opened 1 year ago

Gleefre commented 1 year ago

Hello, I'm trying to make a standalone executable of an app made with sketch. I tried to look at qelt, since it has binaries here (itch.io), but I haven't been able to find a solution.

How can I define a main function? Just doing (make-instance 'app) doesn't work - main thread is closed immediately and the application is close just after starting up.

Gleefre commented 1 year ago

Hi again! So I have been reading through source code and I have discovered *build* variable.

It seems that the following should work (make-this-thread-main as in kit.sdl2 README):

(let ((sketch::*build* t))
  (sdl2:make-this-thread-main
   (lambda ()
     (make-instance 'app))))

However, *build* is not exported, is it fine using it like this?

vydd commented 1 year ago

Hi @Gleefre! I found the script I think I used to build sketch:

(ql:quickload :bordeaux-threads)
(ql:quickload :trivial-dump-core)
(ql:quickload :qelt)

(defparameter *running* t)

(defmethod kit.sdl2:close-window :after ((instance sketch::sketch))
  (setf *running* nil))

(defun run ()
  (sb-ext:disable-debugger)
  (sketch::initialize-sketch)
  (bt:make-thread #'qelt:qelt)
  (loop while *running* do (sleep 1))
  #++
  (let ((thread (find "SDL2 Main Thread" (bt:all-threads)
              :key 'bt:thread-name
              :test 'equalp)))
    (bt:join-thread thread)))

(sb-ext:save-lisp-and-die "qelt.exe" :toplevel #'run :executable t)

.exe was there just for the Windows build I think - not sure if I was changing anything for Linux builds. This is probably not the best way to do it (#++ is definitely an artifact of me trying out multiple things).

It's also not using ::*build* and I don't know why, as it probably is needed for


If you figure it out, feel free to create a PR. Ideally it would be something like sketch:make-executable. I could also invest some time into it relatively soon.

Gleefre commented 1 year ago

Hello! Thanks for your script, I'll give it a look.

I don't think that such a feature should be sketch:make-executable, since it will be working on sbcl only and will be forcing one method of making an executable (asdf:make can be another way of doing).

Instead, I think it should be something similar to sdl2kit define-start-function.

Gleefre commented 1 year ago

I have been thinking on the possibilities of the define-start-function.

I think it must be able to define both toplevel function (to pass to the save-lisp-or-die or be used in combination with asdf:make) and one that can be used from REPL.

Also it would be nice if it could be used to define additional initialization and quit parts (like sdl2-mixer).

I have come up with following usage example:

(define-start-function (start) key-piano (:resizable t)
  (:on-close (app)
    (close-notes (key-piano-notes app)))
  (:start
    (sdl2-mixer:init :wave)
    (sdl2-mixer:open-audio 22050 :s16sys 1 1024)
    (sdl2-mixer:allocate-channels 100))
  (:quit
    (sdl2-mixer:halt-channel -1)
    (sdl2-mixer:close-audio)
    (sdl2-mixer:quit)
    (print 'bye!)))

And a somewhat ugly implementation:

(defmacro define-start-function ((name &optional toplevel-name)
                                 sketch-name initargs
                                 &rest options)
  "If toplevel-name is not specified uses `<name>-toplevel'.
Possible options:
  :setup - defines `sketch:setup' `:before' method
      (:setup (<arg-name>)
        <body>)
  :on-close - defines `kit.sdl2:on-close' `:before' method;
      (:on-close (<arg-name>)
        <body>)
  :start - executed before creating an instance of sketch (on every function call)
      (:start <body>)
  :quit - executed after the instance is closed (only for toplevel function)
      (:quit <body>)"
  (let ((initargs-name (gensym "INITARGS"))
        (toplevel-name (or toplevel-name
                           (intern (concatenate 'string
                                                (symbol-name name)
                                                "-TOPLEVEL")
                                   (symbol-package name)))))
    (flet ((define-method (name allow-other-keys arg &rest body)
             `(defmethod ,name :before ((,@arg ,sketch-name)
                                        ,@(if allow-other-keys
                                              '(&key &allow-other-keys)))
                (declare (ignorable ,@arg))
                ,@body)))
      `(progn
         ,(alexandria:when-let (arg-and-body (cdr (assoc :setup options)))
            (apply #'define-method 'sketch:setup t arg-and-body))
         ,(alexandria:when-let (arg-and-body (cdr (assoc :on-close options)))
            (apply #'define-method 'kit.sdl2:close-window nil arg-and-body))
         (defun ,name (&rest ,initargs-name &key &allow-other-keys)
           (initialize-sketch)
           ,@(cdr (assoc :start options))
           (apply #'make-instance ',sketch-name (append ,initargs-name ',initargs)))
         (defun ,toplevel-name ()
           (sdl2:make-this-thread-main
            (lambda ()
              (let ((*build* t))
                (initialize-sketch)
                ,@(cdr (assoc :start options))
                (make-instance ',sketch-name ,@initargs))))
           ,@(cdr (assoc :quit options)))
         (values ,name ,toplevel-name)))))

What do you think?