[x11_with_sync]
parent
48b430ba35
commit
c6b9893340
88
x11.c
88
x11.c
|
|
@ -59,58 +59,52 @@ int main(int argc, char **argv){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (1) /ICC/client-to-window-manager/XStoreName.html
|
if (window != 0){
|
||||||
** " assigns the name passed to window_name to the specified window "
|
/* (1) /ICC/client-to-window-manager/XStoreName.html
|
||||||
*/
|
** " assigns the name passed to window_name to the specified window "
|
||||||
XStoreName(display, window, "Example Window");
|
|
||||||
|
|
||||||
/* (1) /window/XMapWindow.html
|
|
||||||
** " maps the window and all of its subwindows that have had map requests "
|
|
||||||
**~ NOTE: This makes the window visible on the screen.
|
|
||||||
*/
|
|
||||||
XMapWindow(display, window);
|
|
||||||
|
|
||||||
/*~ NOTE: Main loop */
|
|
||||||
int exit_loop = 0;
|
|
||||||
for (;!exit_loop;){
|
|
||||||
|
|
||||||
/* (1) /event-handling/XPending.html
|
|
||||||
** " returns the number of events that have been received from the X
|
|
||||||
** server but have not been removed from the event queue "
|
|
||||||
**~ NOTE: The docs say this returns the number of events, but in
|
|
||||||
** since that number could grow as we process inputs, so it's more
|
|
||||||
** usually used as a way to check if there is at least one input.
|
|
||||||
*/
|
*/
|
||||||
for (;XPending(display) > 0;){
|
XStoreName(display, window, "Example Window");
|
||||||
/* (1) /event-handling/manipulating-event-queue/XNextEvent.html
|
|
||||||
** " copies the first event from the event queue into the specified
|
/* (1) /window/XMapWindow.html
|
||||||
** XEvent structure and then removes it from the queue "
|
** " maps the window and all of its subwindows that have had map requests "
|
||||||
*/
|
**~ NOTE: This makes the window visible on the screen.
|
||||||
XEvent event;
|
*/
|
||||||
XNextEvent(display, &event);
|
XMapWindow(display, window);
|
||||||
|
|
||||||
|
/*~ NOTE: Main loop */
|
||||||
|
int exit_loop = 0;
|
||||||
|
for (;!exit_loop;){
|
||||||
|
|
||||||
/* (1) /events/structures.html
|
/* (1) /event-handling/XPending.html
|
||||||
** " The XEvent structure is a union of the individual structures
|
** " returns the number of events that have been received from the X
|
||||||
** declared for each event type. Depending on the type, you should
|
** server but have not been removed from the event queue "
|
||||||
** access members of each event by using the XEvent union. "
|
**~ NOTE: The docs say this returns the number of events, but it's
|
||||||
|
** easier, and possibly more reliable to just use it to check if
|
||||||
|
** there is at lesat one input.
|
||||||
*/
|
*/
|
||||||
switch (event.type){
|
for (;XPending(display) > 0;){
|
||||||
case ClientMessage: {
|
/* (1) /event-handling/manipulating-event-queue/XNextEvent.html
|
||||||
Atom atom = event.xclient.data.l[0];
|
** " copies the first event from the event queue into the specified
|
||||||
if (atom == atom__WM_DELETE_WINDOW){
|
** XEvent structure and then removes it from the queue "
|
||||||
exit_loop = 1;
|
*/
|
||||||
}
|
XEvent event;
|
||||||
}break;
|
XNextEvent(display, &event);
|
||||||
|
|
||||||
|
/* (1) /events/structures.html
|
||||||
|
** " The XEvent structure is a union of the individual structures
|
||||||
|
** declared for each event type. Depending on the type, you should
|
||||||
|
** access members of each event by using the XEvent union. "
|
||||||
|
*/
|
||||||
|
switch (event.type){
|
||||||
|
case ClientMessage: {
|
||||||
|
Atom atom = event.xclient.data.l[0];
|
||||||
|
if (atom == atom__WM_DELETE_WINDOW){
|
||||||
|
exit_loop = 1;
|
||||||
|
}
|
||||||
|
}break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (1) /window-information/XGetWindowAttributes.html
|
|
||||||
** " returns the current attributes for the specified window "
|
|
||||||
*/
|
|
||||||
XWindowAttributes window_attr = {0};
|
|
||||||
if (!XGetWindowAttributes(display, window, &window_attr)){
|
|
||||||
printf("XGetWindowAttributes failed\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*~ NOTE: Shutdown */
|
/*~ NOTE: Shutdown */
|
||||||
|
|
|
||||||
158
x11_egl.c
158
x11_egl.c
|
|
@ -171,7 +171,7 @@ int main(int argc, char **argv){
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLSurface *surface = EGL_NO_SURFACE;
|
EGLSurface *surface = EGL_NO_SURFACE;
|
||||||
{
|
if (window != 0){
|
||||||
/* (2) eglChooseConfig
|
/* (2) eglChooseConfig
|
||||||
** " returns in configs a list of all EGL frame buffer configurations
|
** " returns in configs a list of all EGL frame buffer configurations
|
||||||
** that match the attributes specified in attrib_list "
|
** that match the attributes specified in attrib_list "
|
||||||
|
|
@ -218,88 +218,90 @@ int main(int argc, char **argv){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (1) /ICC/client-to-window-manager/XStoreName.html
|
if (surface != 0){
|
||||||
** " assigns the name passed to window_name to the specified window "
|
/* (1) /ICC/client-to-window-manager/XStoreName.html
|
||||||
*/
|
** " assigns the name passed to window_name to the specified window "
|
||||||
XStoreName(display, window, "Example Window");
|
*/
|
||||||
|
XStoreName(display, window, "Example Window");
|
||||||
/* (1) /window/XMapWindow.html
|
|
||||||
** " maps the window and all of its subwindows that have had map requests "
|
|
||||||
**~ NOTE: This makes the window visible on the screen.
|
|
||||||
*/
|
|
||||||
XMapWindow(display, window);
|
|
||||||
|
|
||||||
/* (2) eglMakeCurrent */
|
|
||||||
EGLBoolean make_current_success2 = 0;
|
|
||||||
if (surface != EGL_NO_SURFACE){
|
|
||||||
make_current_success2 = eglMakeCurrent(egl_display, surface, surface, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (2) eglSwapInterval
|
|
||||||
** " Specifies the minimum number of video frames that are displayed before
|
|
||||||
** a buffer swap will occur "
|
|
||||||
*/
|
|
||||||
EGLBoolean swap_interval_success = 0;
|
|
||||||
if (make_current_success2){
|
|
||||||
swap_interval_success = eglSwapInterval(egl_display, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*~ NOTE: Main loop */
|
|
||||||
int exit_loop = 0;
|
|
||||||
for (;!exit_loop;){
|
|
||||||
|
|
||||||
/* (1) /event-handling/XPending.html
|
/* (1) /window/XMapWindow.html
|
||||||
|
** " maps the window and all of its subwindows that have had map requests "
|
||||||
|
**~ NOTE: This makes the window visible on the screen.
|
||||||
|
*/
|
||||||
|
XMapWindow(display, window);
|
||||||
|
|
||||||
|
/* (2) eglMakeCurrent */
|
||||||
|
EGLBoolean make_current_success2 = 0;
|
||||||
|
if (surface != EGL_NO_SURFACE){
|
||||||
|
make_current_success2 = eglMakeCurrent(egl_display, surface, surface, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (2) eglSwapInterval
|
||||||
|
** " Specifies the minimum number of video frames that are displayed before
|
||||||
|
** a buffer swap will occur "
|
||||||
|
*/
|
||||||
|
EGLBoolean swap_interval_success = 0;
|
||||||
|
if (make_current_success2){
|
||||||
|
swap_interval_success = eglSwapInterval(egl_display, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*~ NOTE: Main loop */
|
||||||
|
int exit_loop = 0;
|
||||||
|
for (;!exit_loop;){
|
||||||
|
|
||||||
|
/* (1) /event-handling/XPending.html
|
||||||
** " returns the number of events that have been received from the X
|
** " returns the number of events that have been received from the X
|
||||||
** server but have not been removed from the event queue "
|
** server but have not been removed from the event queue "
|
||||||
**~ NOTE: The docs say this returns the number of events, but in
|
**~ NOTE: The docs say this returns the number of events, but it's
|
||||||
** since that number could grow as we process inputs, so it's more
|
** easier, and possibly more reliable to just use it to check if
|
||||||
** usually used as a way to check if there is at least one input.
|
** there is at lesat one input.
|
||||||
*/
|
*/
|
||||||
for (;XPending(display) > 0;){
|
for (;XPending(display) > 0;){
|
||||||
/* (1) /event-handling/manipulating-event-queue/XNextEvent.html
|
/* (1) /event-handling/manipulating-event-queue/XNextEvent.html
|
||||||
** " copies the first event from the event queue into the specified
|
** " copies the first event from the event queue into the specified
|
||||||
** XEvent structure and then removes it from the queue "
|
** XEvent structure and then removes it from the queue "
|
||||||
*/
|
*/
|
||||||
XEvent event;
|
XEvent event;
|
||||||
XNextEvent(display, &event);
|
XNextEvent(display, &event);
|
||||||
|
|
||||||
/* (1) /events/structures.html
|
/* (1) /events/structures.html
|
||||||
** " The XEvent structure is a union of the individual structures
|
** " The XEvent structure is a union of the individual structures
|
||||||
** declared for each event type. Depending on the type, you should
|
** declared for each event type. Depending on the type, you should
|
||||||
** access members of each event by using the XEvent union. "
|
** access members of each event by using the XEvent union. "
|
||||||
*/
|
*/
|
||||||
switch (event.type){
|
switch (event.type){
|
||||||
case ClientMessage: {
|
case ClientMessage: {
|
||||||
Atom atom = event.xclient.data.l[0];
|
Atom atom = event.xclient.data.l[0];
|
||||||
if (atom == atom__WM_DELETE_WINDOW){
|
if (atom == atom__WM_DELETE_WINDOW){
|
||||||
exit_loop = 1;
|
exit_loop = 1;
|
||||||
}
|
}
|
||||||
}break;
|
}break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (1) /window-information/XGetWindowAttributes.html
|
||||||
|
** " returns the current attributes for the specified window "
|
||||||
|
*/
|
||||||
|
XWindowAttributes window_attr = {0};
|
||||||
|
if (!XGetWindowAttributes(display, window, &window_attr)){
|
||||||
|
printf("XGetWindowAttributes failed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window_attr.width > 0 && window_attr.height > 0){
|
||||||
|
glDrawBuffer(GL_BACK);
|
||||||
|
glViewport(0, 0, window_attr.width, window_attr.height);
|
||||||
|
glClearColor(0.90f, 0.15f, 0.40f, 1.f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (2) eglSwapBuffers
|
||||||
|
** " back-buffered window surface, then the color buffer is copied
|
||||||
|
** (posted) to the native window associated with that surface "
|
||||||
|
*/
|
||||||
|
EGLBoolean swap_success = eglSwapBuffers(egl_display, surface);
|
||||||
|
if (!swap_success){
|
||||||
|
printf("eglSwapBuffers failed\n");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* (1) /window-information/XGetWindowAttributes.html
|
|
||||||
** " returns the current attributes for the specified window "
|
|
||||||
*/
|
|
||||||
XWindowAttributes window_attr = {0};
|
|
||||||
if (!XGetWindowAttributes(display, window, &window_attr)){
|
|
||||||
printf("XGetWindowAttributes failed\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window_attr.width > 0 && window_attr.height > 0){
|
|
||||||
glDrawBuffer(GL_BACK);
|
|
||||||
glViewport(0, 0, window_attr.width, window_attr.height);
|
|
||||||
glClearColor(0.90f, 0.15f, 0.40f, 1.f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (2) eglSwapBuffers
|
|
||||||
** " back-buffered window surface, then the color buffer is copied
|
|
||||||
** (posted) to the native window associated with that surface "
|
|
||||||
*/
|
|
||||||
EGLBoolean swap_success = eglSwapBuffers(egl_display, surface);
|
|
||||||
if (!swap_success){
|
|
||||||
printf("eglSwapBuffers failed\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
#if 0
|
||||||
|
mkdir -p build
|
||||||
|
clang -o build/demo -g x11_with_sync.c -lX11 -lXext
|
||||||
|
exit 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Reading From:
|
||||||
|
** (1) "xlib manual" https://tronche.com/gui/x/xlib/
|
||||||
|
** (2) "X Synchronization Extension Library" \
|
||||||
|
** https://www.x.org/releases/X11R7.6/doc/libXext/synclib.html
|
||||||
|
** (3) "Extended Window Manager Hints" \
|
||||||
|
** https://specifications.freedesktop.org/wm/latest/
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/extensions/sync.h>
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef unsigned int U32;
|
||||||
|
|
||||||
|
void app_draw(void);
|
||||||
|
|
||||||
|
int main(int argc, char **argv){
|
||||||
|
/* (1) /display/opening.html
|
||||||
|
** " The XOpenDisplay() function returns a Display structure that serves
|
||||||
|
** as the connection to the X server and that contains all the
|
||||||
|
** information about that X server. "
|
||||||
|
*/
|
||||||
|
Display *display = XOpenDisplay(0);
|
||||||
|
if (display == 0){
|
||||||
|
printf("XOpenDisplay failed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (2) XSyncQueryExtension
|
||||||
|
** " If display supports the SYNC extension, XSyncQueryExtension
|
||||||
|
** returns True "
|
||||||
|
*/
|
||||||
|
int event_base = 0;
|
||||||
|
int error_base = 0;
|
||||||
|
Status has_sync_ext = 0;
|
||||||
|
if (display != 0){
|
||||||
|
has_sync_ext = XSyncQueryExtension(display, &event_base, &error_base);
|
||||||
|
if (!has_sync_ext){
|
||||||
|
printf("XSyncQueryExtension failed\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (2) XSyncInitialize
|
||||||
|
** " If display supports the SYNC extension, XSyncQueryExtension
|
||||||
|
** returns True "
|
||||||
|
*/
|
||||||
|
int server_event_base = 0;
|
||||||
|
int server_error_base = 0;
|
||||||
|
Status sync_init = 0;
|
||||||
|
if (has_sync_ext){
|
||||||
|
sync_init = XSyncInitialize(display, &server_event_base, &server_error_base);
|
||||||
|
if (!sync_init){
|
||||||
|
printf("XSyncInitialize failed\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (2) XSyncCreateCounter
|
||||||
|
** " creates a counter on the display with the given initial value and
|
||||||
|
** returns the counter ID "
|
||||||
|
*/
|
||||||
|
XSyncCounter xsync_counter = 0;
|
||||||
|
if (has_sync_ext){
|
||||||
|
XSyncValue zero;
|
||||||
|
XSyncMinValue(&zero);
|
||||||
|
xsync_counter = XSyncCreateCounter(display, zero);
|
||||||
|
if (!xsync_counter){
|
||||||
|
printf("XSyncCreateCounter failed\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (1) /window-information/XInternAtom.html
|
||||||
|
** " returns the atom identifier associated with the specified
|
||||||
|
** atom_name string "
|
||||||
|
*/
|
||||||
|
Atom atom__WM_DELETE_WINDOW = XInternAtom(display, "WM_DELETE_WINDOW", False);
|
||||||
|
Atom atom__NET_WM_SYNC_REQUEST = XInternAtom(display, "_NET_WM_SYNC_REQUEST", False);
|
||||||
|
Atom atom__NET_WM_SYNC_REQUEST_COUNTER = XInternAtom(display, "_NET_WM_SYNC_REQUEST_COUNTER", False);
|
||||||
|
|
||||||
|
/* (1) /window/XCreateWindow.html
|
||||||
|
** " The XCreateWindow function creates an unmapped subwindow for a
|
||||||
|
** specified parent window "
|
||||||
|
*/
|
||||||
|
Window window = 0;
|
||||||
|
if (sync_init){
|
||||||
|
Window parent = DefaultRootWindow(display);
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int w = 640;
|
||||||
|
int h = 480;
|
||||||
|
int border_width = 0;
|
||||||
|
int depth = CopyFromParent;
|
||||||
|
unsigned int xclass = InputOutput;
|
||||||
|
Visual *visual = CopyFromParent;
|
||||||
|
unsigned long valuemask = CWEventMask;
|
||||||
|
|
||||||
|
XSetWindowAttributes attributes = {0};
|
||||||
|
attributes.event_mask = StructureNotifyMask;
|
||||||
|
|
||||||
|
window = XCreateWindow(display, parent, x, y, w, h, border_width,
|
||||||
|
depth, xclass, visual, valuemask,
|
||||||
|
&attributes);
|
||||||
|
|
||||||
|
if (window == 0){
|
||||||
|
printf("XCreateWindow failed\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window != 0){
|
||||||
|
/* (1) /ICC/client-to-window-manager/XSetWMProtocols.html
|
||||||
|
** " replaces the WM_PROTOCOLS property on the specified window "
|
||||||
|
*/
|
||||||
|
Atom protocols[] = {
|
||||||
|
atom__WM_DELETE_WINDOW,
|
||||||
|
atom__NET_WM_SYNC_REQUEST,
|
||||||
|
};
|
||||||
|
XSetWMProtocols(display, window, protocols, sizeof(protocols)/sizeof(Atom));
|
||||||
|
|
||||||
|
/* (1) /window-information/XChangeProperty.html
|
||||||
|
** " alters the property for the specified window "
|
||||||
|
**
|
||||||
|
** (3) /ar01s06.html _NET_WM_SYNC_REQUEST_COUNTER
|
||||||
|
** " "
|
||||||
|
*/
|
||||||
|
|
||||||
|
XChangeProperty(display, window, atom__NET_WM_SYNC_REQUEST_COUNTER,
|
||||||
|
XA_CARDINAL, 32, PropModeReplace,
|
||||||
|
(unsigned char*)&xsync_counter, 1);
|
||||||
|
|
||||||
|
/* (1) /ICC/client-to-window-manager/XStoreName.html
|
||||||
|
** " assigns the name passed to window_name to the specified window "
|
||||||
|
*/
|
||||||
|
XStoreName(display, window, "Example Window");
|
||||||
|
|
||||||
|
/* (1) /window/XMapWindow.html
|
||||||
|
** " maps the window and all of its subwindows that have had map requests "
|
||||||
|
**~ NOTE: This makes the window visible on the screen.
|
||||||
|
*/
|
||||||
|
XMapWindow(display, window);
|
||||||
|
|
||||||
|
/*~ NOTE: Main loop */
|
||||||
|
int exit_loop = 0;
|
||||||
|
int has_sync_serial = 0;
|
||||||
|
XSyncValue sync_serial;
|
||||||
|
for (;!exit_loop;){
|
||||||
|
|
||||||
|
/* (1) /event-handling/XPending.html
|
||||||
|
** " returns the number of events that have been received from the X
|
||||||
|
** server but have not been removed from the event queue "
|
||||||
|
**~ NOTE: The docs say this returns the number of events, but it's
|
||||||
|
** easier, and possibly more reliable to just use it to check if
|
||||||
|
** there is at lesat one input.
|
||||||
|
*/
|
||||||
|
for (;XPending(display) > 0;){
|
||||||
|
/* (1) /event-handling/manipulating-event-queue/XNextEvent.html
|
||||||
|
** " copies the first event from the event queue into the specified
|
||||||
|
** XEvent structure and then removes it from the queue "
|
||||||
|
*/
|
||||||
|
XEvent event;
|
||||||
|
XNextEvent(display, &event);
|
||||||
|
|
||||||
|
/* (1) /events/structures.html
|
||||||
|
** " The XEvent structure is a union of the individual structures
|
||||||
|
** declared for each event type. Depending on the type, you should
|
||||||
|
** access members of each event by using the XEvent union. "
|
||||||
|
*/
|
||||||
|
switch (event.type){
|
||||||
|
case ClientMessage: {
|
||||||
|
Atom atom = event.xclient.data.l[0];
|
||||||
|
if (atom == atom__WM_DELETE_WINDOW){
|
||||||
|
exit_loop = 1;
|
||||||
|
}
|
||||||
|
else if (atom == atom__NET_WM_SYNC_REQUEST){
|
||||||
|
U32 serial_lo = event.xclient.data.l[2];
|
||||||
|
U32 serial_hi = event.xclient.data.l[3];
|
||||||
|
XSyncIntsToValue(&sync_serial, serial_lo, serial_hi);
|
||||||
|
has_sync_serial = 1;
|
||||||
|
}
|
||||||
|
}break;
|
||||||
|
|
||||||
|
case ConfigureNotify: {
|
||||||
|
app_draw();
|
||||||
|
}break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*~ NOTE: After processing events, set counter*/
|
||||||
|
if (has_sync_serial){
|
||||||
|
has_sync_serial = 0;
|
||||||
|
/* (2) XSyncSetCounter
|
||||||
|
** " sets counter to value "
|
||||||
|
*/
|
||||||
|
XSyncSetCounter(display, xsync_counter, sync_serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (1) /window-information/XGetWindowAttributes.html
|
||||||
|
** " returns the current attributes for the specified window "
|
||||||
|
*/
|
||||||
|
XWindowAttributes window_attr = {0};
|
||||||
|
if (!XGetWindowAttributes(display, window, &window_attr)){
|
||||||
|
printf("XGetWindowAttributes failed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
app_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*~ NOTE: Shutdown */
|
||||||
|
|
||||||
|
/* (1) /window/XDestroyWindow.html
|
||||||
|
** " destroys the specified window as well as all of its subwindows "
|
||||||
|
*/
|
||||||
|
if (display != 0 && window != 0){
|
||||||
|
XDestroyWindow(display, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (1) /display/closing.html
|
||||||
|
** " To close a display or disconnect from the X server, use
|
||||||
|
** XCloseDisplay(). "
|
||||||
|
*/
|
||||||
|
if (display != 0){
|
||||||
|
XCloseDisplay(display);
|
||||||
|
}
|
||||||
|
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_draw(void){}
|
||||||
Loading…
Reference in New Issue