bohonghuang / claw-raylib

Fully auto-generated Common Lisp bindings to Raylib (4.5/5.0) and Raygui (3.0/4.0) using claw and cffi-object
Apache License 2.0
40 stars 3 forks source link

Flickering - no double buffering..? #4

Open simendsjo opened 1 year ago

simendsjo commented 1 year ago

I ported from claylib to claw-raylib, and I'm seeing flickering. Not sure what's going on here. Even at target-fps 1, I can see lot's of fast flickering. I'm drawing a grid of characters, and it's just as if no double buffering is used at all, and I can see cells flickering. But how can this be? Does Raylib even support non-double buffering?

bohonghuang commented 1 year ago

Can you provide the code where you encountered the issue?

simendsjo commented 1 year ago

Hmm.. I'm having some problems reproducing it in a small testcase. Just writing a grid doesn't trigger the problem. Maybe I have to read some input or do some other work. I recorded a video to show the issue.

https://github.com/bohonghuang/claw-raylib/assets/351519/164bc2dc-196b-4c65-8a19-534f915384fa

simendsjo commented 1 year ago

Ok, I found it. It seems struct values isn't initialized to a default value, so because I didn't supply an alpha to make-color I guess it just used whatever was located in memory, causing the flicker.

This shows the problem (but it's far from minimal). claw-raylib is a bit too low-level for my liking with issues like this :/ But I guess this is as intended and you're focusing on your engine and not a more lispified API for raylib?

#+(or)(ql:quickload :claw-raylib)
(defpackage :flicker
  (:use #:cl)
  (:local-nicknames (#:r #:raylib)))

(in-package :flicker)

(cffi:defcstruct cint
  (value :int))

(cobj:define-cobject-class (:struct cint))

(defun flicker ()
  (r:with-window ("test" (1920 1080))
    (r:set-target-fps 60)
    (let* ((+y-factor+ (/ 25 (/ 3129 64)))
           (size 36)
           (y-size size)
           (x-size (* size +y-factor+))
           (font (r:load-font-ex (uiop:unix-namestring (asdf:system-relative-pathname :sijo-ion "DejaVuSansMono.ttf"))
                                 size
                                 (cobj:pointer-cobject (cffi:null-pointer) 'cint)
                                 250)))
      (do ()
          ((r:window-should-close))
        (r:with-drawing
            (r:clear-background r:+blank+)
          (dotimes (x 80)
            (dotimes (y 50)
              (r:draw-text-ex font "#" (r:make-vector2 :x (coerce (* x x-size) 'single-float)
                                                       :y (coerce (* y y-size) 'single-float))
                              (coerce size 'single-float)
                              (coerce 0 'single-float)
                              ;; NOTE: No alpha causes the flicker
                              (r:make-color :r 255 :g 255 :b 255)))))))))
bohonghuang commented 1 year ago

Ok, I found it. It seems struct values isn't initialized to a default value, so because I didn't supply an alpha to make-color I guess it just used whatever was located in memory, causing the flicker.

Because C structs do not have default values, we cannot set appropriate default parameters for their constructors during code generation. If we were to initialize fields to 0 by default, it would not be suitable for raylib:color because all of its constructor parameters should have a default value of 255. My choice is to leave the unspecified fields uninitialized, which allows for consistent APIs and significantly reduces the overhead of allocating large objects. This is similar to cl:make-array. This is similar to cl:make-array. If you specify :element-type as fixnum, single-float, etc., but do not specify :initial-element, the values in the array are undefined.

This shows the problem (but it's far from minimal). claw-raylib is a bit too low-level for my liking with issues like this :/ But I guess this is as intended and you're focusing on your engine and not a more lispified API for raylib?

I'm not quite sure what you mean by "lispified," but I believe I have struck a good balance between performance and Lisp-like APIs. I rarely encounter the issues you mentioned in my projects because Raylib provides additional creation functions for many types. For example, for raylib:vector2, I often use (raylib:vector2-zero) and (raylib:vector2-one) instead of (raylib:make-vector2 :x 0.0 :y 0.0 :z 0.0). Similarly, for raylib:color, I use (raylib:get-color #xFFFFFF7F) or (raylib:fade raylib:+white+ 0.5) instead of directly using (raylib:make-color :r 255 :g 255 :b 255 :a 127).

simendsjo commented 1 year ago

I'm not quite sure what you mean by "lispified," but I believe I have struck a good balance between performance and Lisp-like APIs. I rarely encounter the issues you mentioned in my projects because Raylib provides additional creation functions for many types.

Yes, I'm probably using it wrong.

I have things like the following in my codebase:

(cffi:defcstruct cint
  (value :int))

(cobj:define-cobject-class (:struct cint))

(cffi:foreign-enum-value enum keyword)

(raylib:make-vector2 :x (coerce (column 0) 'single-float) :y (coerce (row (+ i +map-height+)) 'single-float))

(cobj:pointer-cobject (cffi:null-pointer) 'cint)
bohonghuang commented 1 year ago

I think I understand what you mean. You want to avoid using C-related code and pointers in the code, right? However, it's difficult to avoid them when directly interacting with C libraries like Raylib.

  1. The class definition of cffi-object: Many functions require pointers as arguments and may modify them. I use cffi-object to change them to accept Lisp objects (which actually contain a C pointer) as arguments, and then pass the pointers to the C functions. Therefore, this object itself cannot be used as a value in Lisp. To access its value, you must use accessors like cint-value, as it involves taking the address. That's why you need to use the following definition:

    (cffi:defcstruct cint
     (value :int))
    
    (cobj:define-cobject-class (:struct cint))

    In other words, we need to use something like a box to hold the reference. If you don't like these two definitions, you can define a defstruct-style macro to wrap them, which can be used to define data structures specifically for interacting with the Raylib API. I plan to develop a DSL for Raygui bindings in claw-raylib, and I will likely use similar macros to address this issue.

  2. Usage of enumeration types:

    (cffi:foreign-enum-value enum keyword)

    It is very simple to define Lisp-style constants for enumerations in claw-raylib, so you can define them for claw-raylib yourself:

    (defmacro define-cenum-constants (package)
      (let ((package (find-package package)))
        `(progn . ,(loop :for name :being :the hash-key :in cffi::*default-type-parsers* :using (hash-value type-getter)
                         :nconc (when (find-symbol (symbol-name name) package)
                                  (alexandria:ignore-some-conditions (warning)
                                    (let ((type (funcall type-getter)))
                                      (when (and (not (typep type 'cffi::foreign-enum))
                                                 (typep (cffi::ensure-parsed-base-type type) 'cffi::foreign-enum))
                                        (loop :for keyword :in (cffi:foreign-enum-keyword-list (funcall type-getter))
                                              :for constant := (intern (format nil "+~A-~A+" name keyword) package)
                                              :nconc `((defconstant ,constant ,(cffi:foreign-enum-value type keyword))
                                                       (export ',constant ,package)))))))))))
    
    (define-cenum-constants :raylib)
    (define-cenum-constants :raygui)
    (define-cenum-constants :rlgl)

    However, I prefer to directly use the keyword access method of cffi:foreign-enum-value for the following reasons:

    1. Consistency with bitfields: We can define constants for enumeration types, but this obviously doesn't apply to bitfields, as it involves combinations of each bit.

      (cffi:foreign-bitfield-value 'raylib:config-flags '(:window-resizable :window-undecorated))
    2. With keywords, I can perform key mapping in my engine, for example:

      (defparameter *key-mapping-table* (make-hash-table :test #'eq))
      (defun key (key)
      (cffi:foreign-enum-value 'raylib:keyboard-key (gethash key *key-mapping-table* key)))
      (setf (gethash :w *key-mapping-table*) :up)
      (assert (eq (key :up) (key :w)))
  3. Type coercion?

    (raylib:make-vector2 :x (coerce (column 0) 'single-float) :y (coerce (row (+ i +map-height+)) 'single-float))

    If you need automatic type coercion, you can define your own utility function similar to 3d-vectors:

    (declaim (inline vec))
    (defun vec (x y &optional (z nil vector3-p) (w nil vector4-p))
     (cond
       (vector4-p (raylib:make-vector4 :x (float x) :y (float y) :z (float z) :w (float w)))
       (vector3-p (raylib:make-vector3 :x (float x) :y (float y) :z (float z)))
       (t (raylib:make-vector2 :x (float x) :y (float y)))))
  4. Using null pointers

    (cobj:pointer-cobject (cffi:null-pointer) 'cint)

    You expect (cobj:pointer-cobject (cffi:null-pointer) 'cint) to be replaced with nil, right? This is actually possible by modifying the wrapper, where you can check the pointer arguments in the function and pass (cffi:null-pointer) to the foreign function if an argument is nil. However, I personally don't like this approach as it introduces overhead. I prefer:

    (alexandria:define-constant nullptr (cobj:pointer-cpointer (cffi:null-pointer) nil)
     :test #'cobj:cobject-eq)

    And then pass nullptr to the called function.

simendsjo commented 1 year ago

You want to avoid using C-related code and pointers in the code, right?

Yes, I don't need to squeece out any performance at all, so I prefer convenience over performance.

If you don't like these two definitions, you can define a defstruct-style macro to wrap them, which can be used to define data structures specifically for interacting with the Raylib API. I plan to develop a DSL for Raygui bindings in claw-raylib, and I will likely use similar macros to address this issue.

Yes, I just don't want to define such things myself, only to update the library later to find out you've done the same work just better ;)

It is very simple to define Lisp-style constants for enumerations in claw-raylib, so you can define them for claw-raylib yourself: [ snip defmacro define-cenum-constants .. ]

I had the following macro to define +key-...+ fields, but I found out raygui had these fields defined, so I deleted my code again. I see your point about bitfields, but it's very verbose for little gain imo.

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defmacro intern-enum (enum &optional (prefix ""))
    `(progn
       ,@(mapcar (lambda (keyword)
                   `(defconst ,(intern (str:concat "+" (string-upcase prefix) (symbol-name keyword) "+")) ,(cffi:foreign-enum-value enum keyword)))
               (cffi:foreign-enum-keyword-list enum)))))

With keywords, I can perform key mapping in my engine, for example:

I do this for nkeymaps with a custom conversion function so I can state raylib keys using <the-key> syntax. This way I have C-j for control + j, and C-<up> for control + raygui:+key-up+.

If you need automatic type coercion, you can define your own utility function similar to 3d-vectors:

Thanks, I'll look into that. It's more that I'd like an easy to use API rather than an actual need. claw-raylib seems to work just fine.

(alexandria:define-constant nullptr (cobj:pointer-cpointer (cffi:null-pointer) nil) :test #'cobj:cobject-eq) And then pass nullptr to the called function.

Thanks, I'll add this. But is there a reason why this, cint and so on couldn't be added to the library itself? I notice you define them in the example application too, which is where I found the usage.

bohonghuang commented 1 year ago

I see your point about bitfields, but it's very verbose for little gain imo.

Of course, I'm just giving an example to illustrate that it cannot be implemented using constants. In practice, you can write it like this to avoid redundancy:

(defun config-flags (&rest flags)
  (cffi:foreign-bitfield-value 'raylib:config-flags flags))

(raylib:set-config-flags (config-flags :window-resizable :window-undecorated))

As the name of the repository suggests, I want claw-raylib to remain as minimal as possible and not opinionated. Because in Common Lisp, you can easily add your own extensions to a library (for example, you can put your favorite functions in a library called claw-raylib-pro). Just like the define-cenum-constants macro I wrote above, you can export your own extensions from the raylib package, so using your own extensions is no different from using the original interface of the library.

Yes, I don't need to squeece out any performance at all, so I prefer convenience over performance.

But is there a reason why this, cint and so on couldn't be added to the library itself? I notice you define them in the example application too, which is where I found the usage.

If performance is not a concern, you can also omit the definitions of cint and cfloat in my example and use cobj:make-carray instead. You can build a very generic C type container based on it.

(defun make-cbox (value
                  &optional
                    (type
                     (typecase value
                       (integer '(signed-byte 32))
                       (string 'string)
                       (t (type-of value)))))
  (cobj:make-carray 1 :element-type type :initial-element value))

(defun cbox-pointer (cbox)
  (cobj:cobject-pointer cbox))

(defun cbox-value (cbox)
  (cobj:caref cbox 0))

(defun (setf cbox-value) (value cbox)
  (setf (cobj:caref cbox 0) value))

(let ((box (make-cbox 0.0)))
  (setf (cbox-value box) 1.0)
  (assert (typep (cbox-pointer box) 'cffi:foreign-pointer))
  (assert (= (cbox-value box) 1.0)))