astefanutti / kms-glsl

CLI that runs OpenGL fragment shaders using the DRM/KMS Linux kernel subsystem
https://ttt.io/glsl-raspberry-pi
MIT License
87 stars 14 forks source link

A little patch for shadertoy.c that allows for variables sent through a socket #19

Closed Quasimondo closed 11 months ago

Quasimondo commented 1 year ago

Since I saw that some other people also wanted to be able to pass in variables into a shader, I hacked this patch together (with the help of GPT-4) which adds four uniforms vec4 iAux0, iAux1, iAux2 and iAux3 to the shader code and which can be updated by sending new values via a socket connection (e.g. from a Python script). Since this alters the functionality I did not want to submit this as a pull request, so please the excuse the bloatedness of this post.

It is all very rough and basic, but maybe someone with better C skills than me can turn this into something more universal. Of course you can also alter the variable names or types depending on your needs.

In Makefile you will have to add -lpthread to the libs since the socket runs in a thread, so the line has to be changed to this LDLIBS=-lGLESv2 -lEGL -ldrm -lgbm -lxcb-randr -lxcb -lpthread

Patched shadertoy.c

/*
 * Copyright © 2020 Antonin Stefanutti <antonin.stefanutti@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sub license,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the
 * next paragraph) shall be included in all copies or substantial portions
 * of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#define _GNU_SOURCE

#include <err.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <regex.h>
#include <stdlib.h>

#include <GLES3/gl3.h>

#include "common.h"

/*
* Socket Server Code start
*/

#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

volatile float vec0[] = {0.0f, 0.0f, 0.0f, 0.0f};
volatile float vec1[] = {0.0f, 0.0f, 0.0f, 0.0f};
volatile float vec2[] = {0.0f, 0.0f, 0.0f, 0.0f};
volatile float vec3[] = {0.0f, 0.0f, 0.0f, 0.0f};
volatile int updated = 0;

void* socketServer() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(1234);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    bind(sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr));
    listen(sockfd, 5);

    while(1) {
        int new_sock;
        struct sockaddr_in client_addr;
        socklen_t addr_size = sizeof(client_addr);
        char buffer[1024] = {0};
        printf("\nListening for incoming connections...\n");
        new_sock = accept(sockfd, (struct sockaddr*) &client_addr, &addr_size);

        while(1) {
            memset(buffer, 0, sizeof(buffer));
            int nbytes = read(new_sock, buffer, sizeof(buffer));
            if(nbytes <= -1) { // -1 is returned by read() if there's an error
                perror("read error or connection closed by client");
                break;
            }

            sscanf(buffer, "%f,%f,%f,%f %f,%f,%f,%f %f,%f,%f,%f  %f,%f,%f,%f",
                   &vec0[0], &vec0[1], &vec0[2], &vec0[3],
                   &vec1[0], &vec1[1], &vec1[2], &vec1[3],
                   &vec2[0], &vec2[1], &vec2[2], &vec2[3],
                   &vec3[0], &vec3[1], &vec3[2], &vec3[3]);

            updated = 1;
        }
        close(new_sock);
    }
    return NULL;
}

/*
* Socket Server Code end
*/

GLint iTime, iFrame, iAux0, iAux1, iAux2, iAux3;

static const char *shadertoy_vs_tmpl_100 =
        "// version (default: 1.10)              \n"
        "%s                                      \n"
        "                                        \n"
        "attribute vec3 position;                \n"
        "                                        \n"
        "void main()                             \n"
        "{                                       \n"
        "    gl_Position = vec4(position, 1.0);  \n"
        "}                                       \n";

static const char *shadertoy_vs_tmpl_300 =
        "// version                              \n"
        "%s                                      \n"
        "                                        \n"
        "in vec3 position;                       \n"
        "                                        \n"
        "void main()                             \n"
        "{                                       \n"
        "    gl_Position = vec4(position, 1.0);  \n"
        "}                                       \n";

static const char *shadertoy_fs_tmpl_100 =
        "// version (default: 1.10)                                                           \n"
        "%s                                                                                   \n"
        "                                                                                     \n"
        "#ifdef GL_FRAGMENT_PRECISION_HIGH                                                    \n"
        "precision highp float;                                                               \n"
        "#else                                                                                \n"
        "precision mediump float;                                                             \n"
        "#endif                                                                               \n"
        "                                                                                     \n"
        "uniform vec3      iResolution;           // viewport resolution (in pixels)          \n"
        "uniform float     iTime;                 // shader playback time (in seconds)        \n"
        "uniform int       iFrame;                // current frame number                     \n"
        "uniform vec4      iMouse;                // mouse pixel coords                       \n"
        "uniform vec4      iDate;                 // (year, month, day, time in seconds)      \n"
        "uniform float     iSampleRate;           // sound sample rate (i.e., 44100)          \n"
        "uniform vec3      iChannelResolution[4]; // channel resolution (in pixels)           \n"
        "uniform float     iChannelTime[4];       // channel playback time (in sec)           \n"
        "uniform vec4      iAux0;                 // Auxillary value from socket              \n"
        "uniform vec4      iAux1;                 // Auxillary value from socket              \n"
        "uniform vec4      iAux2;                 // Auxillary value from socket              \n"
        "uniform vec4      iAux3;                 // Auxillary value from socket              \n"
        "                                                                                     \n"
        "// Shader body                                                                       \n"
        "%s                                                                                   \n"
        "                                                                                     \n"
        "void main()                                                                          \n"
        "{                                                                                    \n"
        "    mainImage(gl_FragColor, gl_FragCoord.xy);                                        \n"
        "}                                                                                    \n";

static const char *shadertoy_fs_tmpl_300 =
        "// version                                                                           \n"
        "%s                                                                                   \n"
        "                                                                                     \n"
        "#ifdef GL_FRAGMENT_PRECISION_HIGH                                                    \n"
        "precision highp float;                                                               \n"
        "#else                                                                                \n"
        "precision mediump float;                                                             \n"
        "#endif                                                                               \n"
        "                                                                                     \n"
        "out vec4 fragColor;                                                                  \n"
        "                                                                                     \n"
        "uniform vec3      iResolution;           // viewport resolution (in pixels)          \n"
        "uniform float     iTime;                 // shader playback time (in seconds)        \n"
        "uniform int       iFrame;                // current frame number                     \n"
        "uniform vec4      iMouse;                // mouse pixel coords                       \n"
        "uniform vec4      iDate;                 // (year, month, day, time in seconds)      \n"
        "uniform float     iSampleRate;           // sound sample rate (i.e., 44100)          \n"
        "uniform vec3      iChannelResolution[4]; // channel resolution (in pixels)           \n"
        "uniform float     iChannelTime[4];       // channel playback time (in sec)           \n"
        "uniform vec4      iAux0;                 // Auxillary value from socket              \n"
        "uniform vec4      iAux1;                 // Auxillary value from socket              \n"
        "uniform vec4      iAux2;                 // Auxillary value from socket              \n"
        "uniform vec4      iAux3;                 // Auxillary value from socket              \n"
        "                                                                                     \n"
        "// Shader body                                                                       \n"
        "%s                                                                                   \n"
        "                                                                                     \n"
        "void main()                                                                          \n"
        "{                                                                                    \n"
        "    mainImage(fragColor, gl_FragCoord.xy);                                           \n"
        "}                                                                                    \n";

static const GLfloat vertices[] = {
        // First triangle:
        1.0f, 1.0f,
        -1.0f, 1.0f,
        -1.0f, -1.0f,
        // Second triangle:
        -1.0f, -1.0f,
        1.0f, -1.0f,
        1.0f, 1.0f,
};

static const char *load_shader(const char *file) {
    struct stat statbuf;
    int fd, ret;

    fd = open(file, 0);
    if (fd < 0) {
        err(fd, "could not open '%s'", file);
    }

    ret = fstat(fd, &statbuf);
    if (ret < 0) {
        err(ret, "could not stat '%s'", file);
    }

    return mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
}

#define GLSL_VERSION_REGEX "GLSL[[:space:]]*(ES)?[[:space:]]*([[:digit:]]+)\\.([[:digit:]]+)"

static char *extract_group(const char *str, regmatch_t group) {
    char *c = calloc(group.rm_eo - group.rm_so, sizeof(char));
    memcpy(c, &str[group.rm_so], group.rm_eo - group.rm_so);
    return c;
}

static char *glsl_version() {
    int ret;
    regex_t regex;
    if ((ret = regcomp(&regex, GLSL_VERSION_REGEX, REG_EXTENDED)) != 0) {
        err(ret, "failed to compile GLSL version regex");
    }

    char *version = "";
    const char *glsl_version = (char *) glGetString(GL_SHADING_LANGUAGE_VERSION);
    if (strlen(glsl_version) == 0) {
        printf("Cannot detect GLSL version from %s\n", "GL_SHADING_LANGUAGE_VERSION");
        return version;
    }

    size_t nGroups = 4;
    regmatch_t groups[nGroups];
    ret = regexec(&regex, glsl_version, nGroups, groups, 0);
    if (ret == REG_NOMATCH) {
        printf("Cannot match GLSL version '%s'\n", glsl_version);
    } else if (ret != 0) {
        err(ret, "failed to match GLSL version '%s'", glsl_version);
    } else {
        char *es = extract_group(glsl_version, groups[1]);
        char *major = extract_group(glsl_version, groups[2]);
        char *minor = extract_group(glsl_version, groups[3]);

        if (strcmp(minor, "0") == 0) {
            free(minor);
            minor = malloc(sizeof(char) * 3);
            strcpy(minor, "00");
        }

        bool is100 = strcmp(major, "1") == 0 && strcmp(minor, "00") == 0;
        bool hasES = strcasecmp(es, "ES") == 0 && !is100;

        asprintf(&version, "%s%s%s", major, minor, hasES ? " es" : "");

        free(es);
        free(major);
        free(minor);
    }
    regfree(&regex);

    return version;
}

static void draw_shadertoy(uint64_t start_time, unsigned frame) {
    glUniform1f(iTime, (GLfloat) (get_time_ns() - start_time) / NSEC_PER_SEC);
    // Replace the above to input elapsed time relative to 60 FPS
    // glUniform1f(iTime, (GLfloat) frame / 60.0f);
    glUniform1ui(iFrame, frame);

    if(updated) { // check if vectors has been updated
        /*
        printf("Updated iVecs: [%f, %f, %f, %f] [%f, %f, %f, %f] [%f, %f, %f, %f], [%f, %f, %f, %f]", 
        vec0[0], vec0[1], vec0[2], vec0[3],
        vec1[0], vec1[1], vec1[2], vec1[3],
        vec2[0], vec2[1], vec2[2], vec2[3],
        vec3[0], vec3[1], vec3[2], vec3[3]);
        */
        /*
        printf("Updated iVecs: iAix0: [%f, %f, %f, %f]\niAux1: [%f, %f, %f, %f] ", 
        vec0[0], vec0[1], vec0[2], vec0[3],
        vec1[0], vec1[1], vec1[2], vec1[3]);
        */
        glUniform4fv(iAux0, 1, (const GLfloat*)vec0);    
        glUniform4fv(iAux1, 1, (const GLfloat*)vec1);
        glUniform4fv(iAux2, 1, (const GLfloat*)vec2);
        glUniform4fv(iAux3, 1, (const GLfloat*)vec3);

        // Reset the flag after print
        updated = 0;
    }

    start_perfcntrs();

    glDrawArrays(GL_TRIANGLES, 0, 6);

    end_perfcntrs();
}

int init_shadertoy(const struct gbm *gbm, struct egl *egl, const char *file) {
    int ret;
    char *shadertoy_vs, *shadertoy_fs;
    GLuint program, vbo;
    GLint iResolution;

    const char *shader = load_shader(file);

    const char *version = glsl_version();
    if (strlen(version) > 0) {
        char *invalid;
        long v = strtol(version, &invalid, 10);
        if (invalid == version) {
            printf("failed to parse detected GLSL version: %s\n", invalid);
            return -1;
        }
        char *version_directive;
        asprintf(&version_directive, "#version %s", version);
        printf("Using GLSL version directive: %s\n", version_directive);

        bool is_glsl_3 = v >= 300;
        asprintf(&shadertoy_vs, is_glsl_3 ? shadertoy_vs_tmpl_300 : shadertoy_vs_tmpl_100, version_directive);
        asprintf(&shadertoy_fs, is_glsl_3 ? shadertoy_fs_tmpl_300 : shadertoy_fs_tmpl_100, version_directive, shader);
    } else {
        asprintf(&shadertoy_vs, shadertoy_vs_tmpl_100, version);
        asprintf(&shadertoy_fs, shadertoy_fs_tmpl_100, version, shader);
    }

    ret = create_program(shadertoy_vs, shadertoy_fs);
    if (ret < 0) {
        printf("failed to create program\n");
        return -1;
    }

    program = ret;

    ret = link_program(program);
    if (ret) {
        printf("failed to link program\n");
        return -1;
    }

    pthread_t thread_id;
    pthread_create(&thread_id, NULL, socketServer, NULL);

    glViewport(0, 0, gbm->width, gbm->height);
    glUseProgram(program);
    iTime = glGetUniformLocation(program, "iTime");
    iFrame = glGetUniformLocation(program, "iFrame");
    iAux0 = glGetUniformLocation(program, "iAux0");
    iAux1 = glGetUniformLocation(program, "iAux1");
    iAux2 = glGetUniformLocation(program, "iAux2");
    iAux3 = glGetUniformLocation(program, "iAux3");

    iResolution = glGetUniformLocation(program, "iResolution");
    glUniform3f(iResolution, gbm->width, gbm->height, 0);
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), 0, GL_STATIC_DRAW);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), &vertices[0]);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (const GLvoid *) (intptr_t) 0);
    glEnableVertexAttribArray(0);

    egl->draw = draw_shadertoy;

    return 0;
}

A simple python test script which sends new random values every 2 seconds:

import socket
import time
import random

def generate_random_vector():
    return [random.random() for _ in range(4)]

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 1234)) # replace with your server's IP and port if not running on the same machine

while True: 
    vecs = [generate_random_vector() for _ in range(4)]
    data = ' '.join(','.join(str(v) for v in vec) for vec in vecs)

    client_socket.sendall(data.encode())

    time.sleep(2)

client_socket.close()
astefanutti commented 1 year ago

Thanks a lot! This really gives a good idea of what a socket based approach would look like.

I still need to review the solution space, and probably the problem space a little bit :). I'd really like to come up with a solution that's be generic and versatile.

Anyhow, your proposition is a very good candidate, and gives us a good baseline to start with.

I'll keep you posted here ASAP.

Quasimondo commented 1 year ago

Thanks - I am happy if this motivates you to improve this library since it is really great for standalone work on affordable hardware! But yeah this definitely needs to be generalized more and the socket stuff also needs some more error handling and recovery from disconnect.

astefanutti commented 11 months ago

@Quasimondo I've been able to iterate on this, and finally landed on another approach that provides a Python wrapper around the native library.

I've just merged #21 that contains the bulk of the work. You'll be able to find some examples on how to bring your own inputs in the glsl.py file here:

https://github.com/astefanutti/kms-glsl/blob/f85385532afdddbd0f42e554c94047e8d203f06c/glsl.py#L19-L126

Do not hesitate to share any feedback or ideas you might have 🙏🏼.