Code from part three:

;;; dependencies
(require 'sb-sprof)
(require 'cl-opengl)
(require 'vecto)
(require 'lispbuilder-sdl-image)
(require 'bordeaux-threads)
;;; code

(defmacro restartable (&body body)
  "helper macro since we use continue restarts a lot
 (remember to hit C in slime or pick the restart so errors don't kill the app)"
  `(restart-case
      (progn ,@body)
    (continue () :report "Continue")))

(defun load-a-texture (filename)
  (let ((texture (car (gl:gen-textures 1)))
        (image (sdl-image:load-image filename)))
    (gl:bind-texture :texture-2d texture)
    (gl:tex-parameter :texture-2d :generate-mipmap t) ;; <- new
    (gl:tex-parameter :texture-2d :texture-min-filter :linear-mipmap-linear) ;; <- new
    (sdl-base::with-pixel (pix (sdl:fp image))
      (let ((texture-format (ecase (sdl-base::pixel-bpp pix)
                              (1 :luminance)
                              (2 :luminance-alpha)
                              (3 :rgb)
                              (4 :rgba))))
        (assert (and (= (sdl-base::pixel-pitch pix)
                        (* (sdl:width image) (sdl-base::pixel-bpp pix)))
                     (zerop (rem (sdl-base::pixel-pitch pix) 4))))
        (gl:tex-image-2d :texture-2d 0 :rgba
                         (sdl:width image) (sdl:height image)
                         0
                         texture-format
                         :unsigned-byte (sdl-base::pixel-data pix))))
    texture))

;; physical size of window in pixels
(defparameter *actual-screen-width* 320)
(defparameter *actual-screen-height* 200)
;; nominal size of window in pixels, in case we just want to scale the
;; scene to match the window instead of showing more of the world
(defparameter *nominal-screen-width* 320)
(defparameter *nominal-screen-height* 200)
;; extents of the window in GL coordinates
(defparameter *screen-width* nil)
(defparameter *screen-height* nil)
;; flag specifying how we want to handle chamging resolution:
;;  if T, always show a fixed amount of the world
;;  if NIL, keep 1 pixel on screen = 1 unit of world space, so more of the
;;    world shows when the window gets larger
(defparameter *use-nominal-size* t)

;; store the mouse position somewhere easy to get to
(defparameter *mouse-x* 0)
(defparameter *mouse-y* 0)

(defvar *next-frame-hook-mutex* (bt:make-lock "frame-hook-lock"))
(defparameter *next-frame-hook* nil)
(defmacro next-frame (&body body)
  `(bt:with-lock-held (*next-frame-hook-mutex*)
     (progn (push (lambda () ,@body) *next-frame-hook*))))

(defparameter *click-particles-lifetime-ms* 3000)
(defparameter *click-particles-count* 24)
(defparameter *click-particles-speed* 50) ;; pixels per second

(defparameter *frame-count* 0)
(defparameter *last-fps-message-time* 0)
(defparameter *last-fps-message-frame-count* 0)
(defparameter *fps-message-interval* 2000) ;; in milliseconds

(defparameter *benchmark-start-time* nil)
(defparameter *benchmark-start-frames* 0)
(defparameter *benchmark-duration-ms* *click-particles-lifetime-ms*)
(defparameter *benchmark-message* "")


(defun setup-ortho-projection (width height)
  (setf *actual-screen-width* width)
  (setf *actual-screen-height* height)
  (gl:matrix-mode :projection)
  (gl:load-identity)
  (gl:viewport 0 0 width height)
  (if *use-nominal-size*
    (setf *screen-width* *nominal-screen-width*
          *screen-height* *nominal-screen-height*)
    (setf *screen-width* *actual-screen-width*
          *screen-height* *actual-screen-height*))
  ;; this sets up the world so the screen coordinates go from 0,0 at lower
  ;; left to width,height at upper right
  (gl:ortho 0 *screen-width*  *screen-height* 0  0 1)
  (gl:matrix-mode :modelview))

(defclass particle-system ()
  ())

(defgeneric draw-object (object)
  (:documentation "draw object to screen")
  (:method ((object particle-system))))

(defgeneric update-object (object delta-t)
  (:documentation "update state of object by delta-t milliseconds, return NIL if
object is no longer active and should be removed, T otherwise")
  (:method ((object particle-system) delta-t) nil))


(defclass particle-manager ()
  ((systems :initform nil :accessor systems)))

(defmethod draw-object ((object particle-manager))
  (mapc 'draw-object (systems object)))

(defmethod update-object ((object particle-manager) delta-t)
  (setf (systems object)
        (delete-if-not (lambda (x) (update-object x delta-t))
                       (systems object))))

(defparameter *particle-manager* (make-instance 'particle-manager))


(defclass managed-texture ()
  ((name :initform nil :initarg :name :accessor name)
   (target :initform :texture-2d :accessor target)))

(defgeneric create-texture (texture)
  (:documentation "Create the GL texture name, and upload the texture data"))
(defgeneric bind-texture (texture)
  (:documentation "Bind the texture"))
(defgeneric delete-texture (texture)
  (:documentation "Release the GL texture name, if previously allocated"))

(defmethod bind-texture ((texture managed-texture))
  (gl:bind-texture (target texture) (name texture)))

(defmethod delete-texture ((texture managed-texture))
  (when (name texture)
    (let ((name (name texture)))
      (setf (name texture) nil)
      ;; newer versions of cl-opengl can automatically check for
      ;; errors, but this function might legitimately be called
      ;; after GL is shut down, so ignore that error if applicable
      (handler-case
          (gl:delete-textures (list name))
        ;; (putting :cl-opengl-checks-errors on *features* was added
        ;;  after the error checks, so if you get errors anyway, try
        ;;  updating cl-opengl)
        #+cl-opengl-checks-errors(%gl::opengl-error (c) (values nil c))))))

(defclass file-texture (managed-texture)
  ((filename :initarg filename :accessor filename)))

(defmethod create-texture ((texture file-texture))
  ;; these could reuse the name if we were sure it was still valid,
  ;; but just to be safe we recreate it
  (when (name texture)
    (gl:delete-textures (list (name texture))))
  (setf (name texture) (load-a-texture (filename texture))))

(defclass sequence-texture (managed-texture)
  ;; we keep a copy of the image data so we can recreate the texture
  ;; if needed, for example after recreating the main window
  ((width :initarg :width :accessor width)
   (height :initarg :height :accessor height)
   (image-data :initarg :image-data :accessor image-data)))

(defmethod create-texture ((texture sequence-texture))
  (when (name texture)
    (gl:delete-textures (list (name texture))))
  (let ((name (car (gl:gen-textures 1))))
    (gl:bind-texture :texture-2d name)
    (gl:tex-parameter :texture-2d :texture-mag-filter :linear)
    (gl:tex-parameter :texture-2d :generate-mipmap t)
    (gl:tex-parameter :texture-2d :texture-min-filter :linear-mipmap-linear)
    (gl:tex-image-2d :texture-2d 0 :rgba (width texture) (height texture)
                     0 :rgba :unsigned-byte
                     (image-data texture))
    (setf (name texture) name)) )

(defclass texture-manager ()
  ((id-map :initform (make-hash-table) :accessor id-map)))

(defgeneric reload-textures (manager)
  (:documentation
   "(re)create GL data for all textures managed by this texture manager"))

(defgeneric unload-textures (manager)
  (:documentation
   "release any GL resources associated with textures managed by this texture manager"))

(defgeneric get-texture-by-id (manager id)
  (:documentation
   "return the texture corresponding to the (EQL comparable) ID"))

(defgeneric (setf get-texture-by-id) (texture manager id)
  (:documentation
   "add or replace the texture corresponding tothe specified ID"))

(defmethod reload-textures ((manager texture-manager))
  (loop for i being the hash-values of (id-map manager)
        do (create-texture i)))

(defmethod unload-textures ((manager texture-manager))
  (loop for i being the hash-values of (id-map manager)
        do (delete-texture i)))

(defmethod get-texture-by-id ((manager texture-manager) id)
  (gethash id (id-map manager)))

(defmethod (setf get-texture-by-id) (texture (manager texture-manager) id)
  (let ((old-texture (get-texture-by-id manager id)))
    ;; if we had a texture with that ID already, and it wasn't listed
    ;; under any other ID, unload it before replacing it
    (when (and old-texture (not (eq texture old-texture))
               (loop for v being the hash-values of (id-map manager)
                  never (eq old-texture v)))
      (delete-texture old-texture)))
  (setf (gethash id (id-map manager)) texture))


(defparameter *texture-manager* nil)

(defmethod bind-texture ((texture symbol))
  (let ((tex (get-texture-by-id *texture-manager* texture)))
    (when (and tex (not (symbolp tex)))
      (bind-texture tex))))

(defmethod create-texture ((texture symbol))
  (let ((tex (get-texture-by-id *texture-manager* texture)))
    (when (and tex (not (symbolp tex)))
      (create-texture tex))))

(defmethod delete-texture ((texture symbol))
  (let ((tex (get-texture-by-id *texture-manager* texture)))
    (when (and tex (not (symbolp tex)))
      (delete-texture tex))))


(defmacro with-texture-manager (&body body)
  (let ((manager (gensym "MANAGER-")))
    `(let* ((,manager (make-instance 'texture-manager))
            (*texture-manager* ,manager))
       (unwind-protect
            (progn
              ,@body)
         (unload-textures ,manager)))))

(defun file-texture (id filename)
  (setf (get-texture-by-id *texture-manager* id)
        (make-instance 'file-texture :filename filename)))

(defmacro vecto-texture ((id width height) &body body)
  `(vecto:with-canvas (:width ,width :height ,height)
     ;; run some vecto code
     ,@body
     ;; and create a texture with (a copy of) the results
     (setf (get-texture-by-id *texture-manager* ,id)
           (make-instance 'sequence-texture :width ,width :height ,height
                          :image-data (copy-seq (vecto::image-data vecto::*graphics-state*))))))


(defclass click-particles (particle-system)
  ((age :initform 0 :accessor age)
   (positions :accessor positions)
   (velocities :accessor velocities)
   ;; add a color slot per particle system instance
   (color :initform (list (random 1.0) (random 1.0) (random 1.0) 0.01) :accessor color))
  (:default-initargs :count *click-particles-count*
    :speed *click-particles-speed*))

;; set the per-system color before drawing the system
(defmethod draw-object :before ((o click-particles))
  (gl:blend-func :src-alpha :one)
  (apply 'gl:color (color o)))

(defmethod initialize-instance :after ((o click-particles) &key count speed &allow-other-keys)
  (let ((positions
         (make-array (list (* 2 count)) :element-type 'single-float :initial-element 0.0))
        (velocities
         (make-array (list (* 2 count)) :element-type 'single-float :initial-element 0.0)))
    (loop
       for i below count
       for j = (* i 2)
       ;; we vary the speed randomly per particle so it isn't just a solid ring
       for v = (* (+ 0.1 (random 1.5)) speed)
       do (setf (aref positions (+ j 0)) (float *mouse-x* 1.0))
       do (setf (aref positions (+ j 1)) (float *mouse-y* 1.0))
       do (setf (aref velocities (+ j 0))
                (float (* v (sin (* i (/ (* 2 pi) count)))) 1.0))
       do (setf (aref velocities (+ j 1))
                (float (* v (cos (* i (/ (* 2 pi) count)))) 1.0)))
    (setf (positions o) positions)
    (setf (velocities o) velocities)))


(defmethod draw-object ((o click-particles))
  (declare (optimize speed))
  (flet ((rectangle (x y width height
                       &optional (u1 0.0) (v1 0.0) (u2 1.0) (v2 1.0))
           (let* ((w/2 (/ width 2.0))
                  (h/2 (/ height 2.0))
                  (x1 (- x w/2))
                  (x2 (+ x w/2))
                  (y1 (- y h/2))
                  (y2 (+ y h/2)))
             (gl:tex-coord u1 v2)
             (gl:vertex x1 y1 0.0)
             (gl:tex-coord u2 v2)
             (gl:vertex x2 y1 0.0)
             (gl:tex-coord u2 v1)
             (gl:vertex x2 y2 0.0)
             (gl:tex-coord u1 v1)
             (gl:vertex x1 y2 0.0))))
    (bind-texture :click-particle)
    (gl:with-primitive :quads
      (loop with positions of-type (simple-array single-float (*)) = (positions o)
         for i below (length positions) by 2
         for x = (aref positions i)
         for y = (aref positions (1+ i))
         do (rectangle x y 32 32)))))

(defmethod update-object ((o click-particles) delta-t)
  (incf (age o) delta-t)
  (setf delta-t (float delta-t 1.0))
  (if (< (age o) *click-particles-lifetime-ms*)
      ;; not done, update particles, and return a non-nil value
      (let ((positions (positions o))
            (velocities (velocities o)))
        (declare (optimize speed)
                 ((simple-array single-float (*)) positions velocities))
        (map-into positions (lambda (p v)
                              (+ p (* v delta-t 0.001)))
                  positions velocities))
      ;; particle system is done, remove it
      nil))


(defclass vbo-click-particles (click-particles)
  ((vbo :initform (car (gl:gen-buffers 1)) :accessor vbo)))



(defmethod draw-object ((o vbo-click-particles))
  (declare (optimize speed))
  (flet ((vbo-rectangle (pointer offset x y width height
                                 &optional (u1 0.0) (v1 0.0) (u2 1.0) (v2 1.0))
           (let* ((w/2 (/ width 2.0))
                  (h/2 (/ height 2.0))
                  (x1 (- x w/2))
                  (x2 (+ x w/2))
                  (y1 (- y h/2))
                  (y2 (+ y h/2)))
             (macrolet
                 ;; we rewrite the macro here a bit to simplify type
                 ;; inference on the offset values
                 ((store-values (&rest v)
                    `(progn ,@(loop for i in v
                                 for j from 0
                                 collect `(setf (cffi:mem-aref pointer
                                                               :float (+ offset ,j))
                                                (float ,i 0.0))))))
               (store-values u1 v2  x1 y1
                             u2 v2  x2 y1
                             u2 v1  x2 y2
                             u1 v1  x1 y2)))))
    (let ((components-per-vertex 4)
          (size-of-float 4)
          (vertices-per-sprite 4))
      (gl:bind-buffer :array-buffer (vbo o))
      (%gl:buffer-data :array-buffer
                       (* (/ (length (positions o)) 2)
                          vertices-per-sprite
                          components-per-vertex
                          size-of-float)
                       (cffi:null-pointer)
                       :stream-draw)
      (gl:with-mapped-buffer (p :array-buffer :write-only)
        ;; we specify the type for the array here too
        (loop with positions of-type (simple-array single-float (*)) = (positions o)
           with length = (length positions)
           for i below length by 2
           ;; and a type for offset
           for offset fixnum from 0 by (* components-per-vertex vertices-per-sprite)
           for x = (aref positions i)
           for y = (aref positions (1+ i))
           do (vbo-rectangle p offset x y 32 32)))
      ;; draw the object
      (%gl:tex-coord-pointer 2 :float
                             (* components-per-vertex size-of-float)
                             (cffi:null-pointer))
      (%gl:vertex-pointer 2 :float
                          (* components-per-vertex size-of-float)
                          (cffi:make-pointer (* 2 size-of-float)))
      (gl:enable-client-state :texture-coord-array)
      (gl:enable-client-state :vertex-array)
      (%gl:draw-arrays :quads 0 (* vertices-per-sprite
                                   (floor (length (positions o)) 2)))
      (gl:disable-client-state :vertex-array)
      (gl:disable-client-state :texture-coord-array)))
  (gl:bind-buffer :array-buffer 0))

;; you may want to set these lower :)
(defparameter *benchmark-particles-per-system* 1000)
(defparameter *benchmark-system-count* 50)
;; copy the random state so we get repeatable results
(defparameter *benchmark-random-state* (make-random-state))

(defun start-benchmark (type)
  (setf *benchmark-start-time* (sdl:sdl-get-ticks))
  (setf *benchmark-start-frames* *frame-count*)
  (setf *benchmark-message* (format nil "~s:" type))
  (let ((*random-state* (make-random-state *benchmark-random-state*)))
    (loop repeat *benchmark-system-count*
       do (let ((*mouse-x* (random *screen-width*))
                (*mouse-y* (random *screen-height*)))
            (push (make-instance type :count *benchmark-particles-per-system*)
                  (systems *particle-manager*))))))

(defun profile-benchmark (type &key (count 1))
  (sb-sprof:reset)
  (sb-sprof:start-profiling)
  (loop repeat count
     do (next-frame (start-benchmark type))
     do (sleep 3))
  (sb-sprof:stop-profiling)
  (format t "profile for ~s~%" type)
  (sb-sprof:report :type :graph))

(defun key-up (key state mod-key scancode unicode)
  (declare (ignore state mod-key scancode unicode))
  (case key
    (:sdl-key-escape (sdl:push-quit-event))
    (:sdl-key-1 (start-benchmark 'click-particles))
    (:sdl-key-2 (start-benchmark 'vbo-click-particles))
    (:sdl-key-3 (profile-benchmark 'click-particles :count 2))
    (:sdl-key-4 (profile-benchmark 'vbo-click-particles :count 2))))

(defun key-down (key state mod-key scancode unicode)
  (declare (ignore key state mod-key scancode unicode)))

(defun mouse-up (button state x y)
  (declare (ignore button state x y)))
(defun mouse-move (x y delta-x delta-y)
  (declare (ignore x y delta-x delta-y)))


(defun mouse-down (button state x y)
  (declare (ignore button state x y))
  (push (make-instance 'vbo-click-particles) (systems *particle-manager*)))


(defun init ()
  (setf (sdl:frame-rate) 0)
  (setf *frame-count* 0)
  (setf *last-fps-message-time* (sdl:sdl-get-ticks))
  (vecto-texture (:click-particle 64 64)
    (vecto:set-rgba-fill 0 0 0 0)
    (vecto:clear-canvas)
    (loop for i below 16
         do
         (vecto:set-rgba-stroke 1 1 1 (/ 1 16.0))
         (vecto:set-line-width i)
         (vecto:centered-circle-path 32 32 20)
         (vecto:stroke)))
  (reload-textures *texture-manager*))

(defun draw ()
  (gl:clear :color-buffer-bit)
  (gl:enable :texture-2d :blend)
  (gl:blend-func :src-alpha :one-minus-src-alpha)
  (draw-object *particle-manager*)
  (sdl:update-display))

(defun update-fps ()
  ;; update the frame count
  (incf *frame-count*)
  ;; handle tick count wrapping to 0
  (when (< (sdl:sdl-get-ticks) *last-fps-message-time*)
    (setf *last-fps-message-time* (sdl:sdl-get-ticks)))
  ;; see if it is time for next message
  (when (>= (sdl:sdl-get-ticks)
           (+ *last-fps-message-time* *fps-message-interval*))
    (let ((frames (- *frame-count* *last-fps-message-frame-count*))
          (seconds (/ (- (sdl:sdl-get-ticks) *last-fps-message-time*) 1000.0)))
      (format t "~s seconds: ~s fps, ~s ms per frame~%"
              seconds
              (if (zerop seconds) "<infinite>" (/ frames seconds))
              (if (zerop frames) "<infinite>" (* 1000 (/ seconds frames)))))
    (setf *last-fps-message-time* (sdl:sdl-get-ticks))
    (setf *last-fps-message-frame-count* *frame-count*)))

(defun update-benchmark ()
  (when (and *benchmark-start-time*
             (> (sdl:sdl-get-ticks)
                (+ *benchmark-start-time* *benchmark-duration-ms*)))
    (let ((frames (- *frame-count* *benchmark-start-frames*))
          (seconds (/ (- (sdl:sdl-get-ticks) *benchmark-start-time*) 1000.0)))
      (format t "~arendered ~s frames in ~s seconds, ~s fps, ~sms/frame~%"
              *benchmark-message*
              frames seconds
              (if (zerop seconds) "infinite" (/ frames seconds))
              (if (zerop frames) "infinite" (* 1000.0 (/ seconds frames))))
      (setf *benchmark-start-time* nil))))

(defun update (delta-t)
  (update-fps)
  (update-benchmark)
  (update-object *particle-manager* delta-t))


(defgeneric delete-particle-system (object))
(defmethod delete-particle-system ((object vbo-click-particles))
  (gl:delete-buffers (list (vbo object))))

(defgeneric delete-particle-systems (manager))
(defmethod delete-particle-systems ((manager particle-manager))
  (mapc 'delete-particle-system (systems manager)))

(defmacro with-particle-manager (&body body)
  (let ((manager (gensym "PARTICLE-MANAGER-")))
    `(let* ((,manager (make-instance 'particle-manager))
            (*particle-manager* ,manager))
       (unwind-protect
            (progn
              ,@body)
         (delete-particle-systems ,manager)))))

(defun main-loop ()
  (sdl:with-init ()
    (sdl:window *nominal-screen-width* *nominal-screen-height*
                :flags (logior sdl:sdl-opengl
                               sdl:sdl-resizable))
    (setf cl-opengl-bindings:*gl-get-proc-address* #'sdl-cffi::sdl-gl-get-proc-address)
    (let ((previous-tick (sdl:sdl-get-ticks)))
      (flet ((mx (x) ;; adjust mouse coordinates from screen to world
               (* x (/ (float *screen-width* 1.0) *actual-screen-width*)))
             (my (y) ;; adjust mouse coordinates from screen to world
               (* y (/ (float *screen-height* 1.0) *actual-screen-height*))))
        (with-texture-manager
          (with-particle-manager
            (init)
            (setup-ortho-projection *nominal-screen-width*
                                    *nominal-screen-height*)
            (sdl:with-events ()
              (:quit-event () t)
              (:video-resize-event (:w w :h h)
                (sdl:resize-window w h)
                (reload-textures *texture-manager*)
                (restartable (setup-ortho-projection w h)))
              (:key-down-event (:state state :scancode scancode :key key
                              :mod-key mod-key :unicode unicode)
                (restartable (key-down key state mod-key scancode unicode)))
              (:key-up-event (:state state :scancode scancode :key key
                              :mod-key mod-key :unicode unicode)
                (restartable (key-up key state mod-key scancode unicode)))
              (:mouse-button-up-event (:button button :state state :x x :y y)
                (restartable (mouse-up button state (mx x) (my y))))
              (:mouse-button-down-event (:button button :state state :x x :y y)
                (restartable (mouse-down button state (mx x) (my y))))
              (:mouse-motion-event (:x x :y y :x-rel delta-x :y-rel delta-y)
                (setf *mouse-x* (mx x)
                      *mouse-y* (my y))
                (restartable (mouse-move (mx x) (my y)
                                         (mx delta-x) (my delta-y))))
              (:idle ()
                     #+(and sbcl (not sb-thread))(restartable
                                                   (sb-sys:serve-all-events 0))
                     (let ((delta-t (- (sdl:sdl-get-ticks) previous-tick)))
                       (setf previous-tick (sdl:sdl-get-ticks))
                       ;; we check for negative delta-t in case sdl's
                       ;; timer wraps after some amount of time, 0 for
                       ;; a frame is better a large negative number
                       (restartable (update (if (minusp delta-t) 0 delta-t))))
                     (restartable (draw))
                     (restartable
                       (bt:with-lock-held (*next-frame-hook-mutex*)
                         (loop for i in *next-frame-hook*
                               do (funcall i))
                         (setf *next-frame-hook* nil)))))))))))

(main-loop)

(profile-benchmark 'click-particles :count 2)

(profile-benchmark 'vbo-click-particles :count 2)