spirv: Add a shared libclc loader

Reviewed-by: Jesse Natalie <jenatali@microsoft.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/7034>
diff --git a/meson_options.txt b/meson_options.txt
index bf031ee..5bbf28f 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -157,6 +157,13 @@
   description : 'build gallium "clover" OpenCL frontend with SPIR-V binary support.',
 )
 option(
+  'static-libclc',
+  type : 'array',
+  value : [],
+  choices : ['spirv', 'spirv64', 'all'],
+  description : 'Link libclc SPIR-V statically.',
+)
+option(
   'd3d-drivers-path',
   type : 'string',
   value : '',
diff --git a/src/compiler/Makefile.sources b/src/compiler/Makefile.sources
index 0c69218..f2051bf 100644
--- a/src/compiler/Makefile.sources
+++ b/src/compiler/Makefile.sources
@@ -376,6 +376,7 @@
 	spirv/GLSL.ext.AMD.h \
 	spirv/GLSL.std.450.h \
 	spirv/gl_spirv.c \
+	spirv/nir_load_libclc.c \
 	spirv/nir_spirv.h \
 	spirv/OpenCL.std.h \
 	spirv/spirv.h \
diff --git a/src/compiler/nir/meson.build b/src/compiler/nir/meson.build
index 992047f..828b235 100644
--- a/src/compiler/nir/meson.build
+++ b/src/compiler/nir/meson.build
@@ -251,6 +251,7 @@
   '../spirv/GLSL.ext.AMD.h',
   '../spirv/GLSL.std.450.h',
   '../spirv/gl_spirv.c',
+  '../spirv/nir_load_libclc.c',
   '../spirv/nir_spirv.h',
   '../spirv/OpenCL.std.h',
   '../spirv/spirv.h',
@@ -266,13 +267,62 @@
   '../spirv/vtn_variables.c',
 )
 
+_libnir_args = []
+if dep_clc.found()
+  _basedir = dep_clc.get_variable(pkgconfig : 'libexecdir')
+
+  _static_libclc = get_option('static-libclc')
+  if _static_libclc.length() > 0
+    if _static_libclc.contains('all')
+      _static_libclc = ['spirv', 'spirv64']
+    endif
+
+    prog_zstd = find_program('zstd', required : false)
+    _zstd_static_libclc = dep_zstd.found() and prog_zstd.found()
+    if _zstd_static_libclc
+      _libnir_args += '-DHAVE_STATIC_LIBCLC_ZSTD'
+    endif
+
+    foreach s : _static_libclc
+      _libnir_args += '-DHAVE_STATIC_LIBCLC_@0@'.format(s.to_upper())
+      f = '@0@-mesa3d-.spv'.format(s)
+      _libclc_file = _basedir / f
+
+      if _zstd_static_libclc
+        _libclc_file = custom_target(
+          '@0@.zstd'.format(f),
+          command : [prog_zstd, '-f', '@INPUT@', '-o', '@OUTPUT@'],
+          input : [_libclc_file],
+          output : '@0@.zstd'.format(f),
+        )
+      endif
+
+      files_libnir += custom_target(
+        '@0@.h'.format(f),
+        command : [
+          prog_python, files_xxd, '-b', '@INPUT@', '@OUTPUT@',
+          '-n', 'libclc_@0@_mesa3d_spv'.format(s),
+        ],
+        input : [_libclc_file],
+        output : '@0@.h'.format(f),
+        depend_files : files_xxd,
+      )
+    endforeach
+  else
+    _libnir_args += ['-DDYNAMIC_LIBCLC_PATH="@0@/"'.format(_basedir)]
+    if not cc.has_function('mmap')
+      error('mmap required for dynamic libCLC loading')
+    endif
+  endif
+endif
+
 _libnir = static_library(
   'nir',
   [files_libnir, spirv_info_c, nir_opt_algebraic_c, nir_opcodes_c,
    nir_opcodes_h, nir_constant_expressions_c, nir_builder_opcodes_h,
    vtn_gather_types_c, nir_intrinsics_c, nir_intrinsics_h],
   include_directories : [inc_include, inc_src, inc_mapi, inc_mesa, inc_gallium, inc_gallium_aux, inc_compiler, include_directories('../spirv')],
-  c_args : [c_msvc_compat_args, no_override_init_args],
+  c_args : [c_msvc_compat_args, no_override_init_args, _libnir_args],
   gnu_symbol_visibility : 'hidden',
   link_with : libcompiler,
   build_by_default : false,
diff --git a/src/compiler/spirv/nir_load_libclc.c b/src/compiler/spirv/nir_load_libclc.c
new file mode 100644
index 0000000..18da4b9
--- /dev/null
+++ b/src/compiler/spirv/nir_load_libclc.c
@@ -0,0 +1,310 @@
+/*
+ * Copyright © 2020 Intel Corporation
+ *
+ * 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, sublicense,
+ * 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 NONINFRINGEMENT.  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.
+ */
+
+#include "nir.h"
+#include "nir_serialize.h"
+#include "nir_spirv.h"
+#include "util/mesa-sha1.h"
+
+#ifdef DYNAMIC_LIBCLC_PATH
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#endif
+
+#ifdef HAVE_STATIC_LIBCLC_ZSTD
+#include <zstd.h>
+#endif
+
+#ifdef HAVE_STATIC_LIBCLC_SPIRV
+#include "spirv-mesa3d-.spv.h"
+#endif
+
+#ifdef HAVE_STATIC_LIBCLC_SPIRV64
+#include "spirv64-mesa3d-.spv.h"
+#endif
+
+struct clc_file {
+   unsigned bit_size;
+   const char *static_data;
+   size_t static_data_size;
+   const char *sys_path;
+};
+
+static const struct clc_file libclc_files[] = {
+   {
+      .bit_size = 32,
+#ifdef HAVE_STATIC_LIBCLC_SPIRV
+      .static_data = libclc_spirv_mesa3d_spv,
+      .static_data_size = sizeof(libclc_spirv_mesa3d_spv),
+#endif
+#ifdef DYNAMIC_LIBCLC_PATH
+      .sys_path = DYNAMIC_LIBCLC_PATH "spirv-mesa3d-.spv",
+#endif
+   },
+   {
+      .bit_size = 64,
+#ifdef HAVE_STATIC_LIBCLC_SPIRV64
+      .static_data = libclc_spirv64_mesa3d_spv,
+      .static_data_size = sizeof(libclc_spirv64_mesa3d_spv),
+#endif
+#ifdef DYNAMIC_LIBCLC_PATH
+      .sys_path = DYNAMIC_LIBCLC_PATH "spirv64-mesa3d-.spv",
+#endif
+   },
+};
+
+static const struct clc_file *
+get_libclc_file(unsigned ptr_bit_size)
+{
+   assert(ptr_bit_size == 32 || ptr_bit_size == 64);
+   return &libclc_files[ptr_bit_size / 64];
+}
+
+struct clc_data {
+   const struct clc_file *file;
+
+   unsigned char cache_key[20];
+
+   int fd;
+   const void *data;
+   size_t size;
+};
+
+static bool
+open_clc_data(struct clc_data *clc, unsigned ptr_bit_size)
+{
+   memset(clc, 0, sizeof(*clc));
+   clc->file = get_libclc_file(ptr_bit_size);
+   clc->fd = -1;
+
+   if (clc->file->static_data) {
+      snprintf((char *)clc->cache_key, sizeof(clc->cache_key),
+               "libclc-spirv%d", ptr_bit_size);
+      return true;
+   }
+
+#ifdef DYNAMIC_LIBCLC_PATH
+   if (clc->file->sys_path != NULL) {
+      int fd = open(clc->file->sys_path, O_RDONLY);
+      if (fd < 0)
+         return false;
+
+      struct stat stat;
+      int ret = fstat(fd, &stat);
+      if (ret < 0) {
+         fprintf(stderr, "fstat failed on %s: %m\n", clc->file->sys_path);
+         close(fd);
+         return false;
+      }
+
+      struct mesa_sha1 ctx;
+      _mesa_sha1_init(&ctx);
+      _mesa_sha1_update(&ctx, clc->file->sys_path, strlen(clc->file->sys_path));
+      _mesa_sha1_update(&ctx, &stat.st_mtim, sizeof(stat.st_mtim));
+      _mesa_sha1_final(&ctx, clc->cache_key);
+
+      clc->fd = fd;
+
+      return true;
+   }
+#endif
+
+   return false;
+}
+
+#define SPIRV_WORD_SIZE 4
+
+static bool
+map_clc_data(struct clc_data *clc)
+{
+   if (clc->file->static_data) {
+#ifdef HAVE_STATIC_LIBCLC_ZSTD
+      unsigned long long cmp_size =
+         ZSTD_getFrameContentSize(clc->file->static_data,
+                                  clc->file->static_data_size);
+      if (cmp_size == ZSTD_CONTENTSIZE_UNKNOWN ||
+          cmp_size == ZSTD_CONTENTSIZE_ERROR) {
+         fprintf(stderr, "Could not determine the decompressed size of the "
+                         "libclc SPIR-V\n");
+         return false;
+      }
+
+      size_t frame_size =
+         ZSTD_findFrameCompressedSize(clc->file->static_data,
+                                      clc->file->static_data_size);
+      if (ZSTD_isError(frame_size)) {
+         fprintf(stderr, "Could not determine the size of the first ZSTD frame "
+                         "when decompressing libclc SPIR-V: %s\n",
+                 ZSTD_getErrorName(frame_size));
+         return false;
+      }
+
+      void *dest = malloc(cmp_size + 1);
+      size_t size = ZSTD_decompress(dest, cmp_size, clc->file->static_data,
+                                    frame_size);
+      if (ZSTD_isError(size)) {
+         free(dest);
+         fprintf(stderr, "Error decompressing libclc SPIR-V: %s\n",
+                 ZSTD_getErrorName(size));
+         return false;
+      }
+
+      clc->data = dest;
+      clc->size = size;
+#else
+      clc->data = clc->file->static_data;
+      clc->size = clc->file->static_data_size;
+#endif
+      return true;
+   }
+
+#ifdef DYNAMIC_LIBCLC_PATH
+   if (clc->file->sys_path != NULL) {
+      off_t len = lseek(clc->fd, 0, SEEK_END);
+      if (len % SPIRV_WORD_SIZE != 0) {
+         fprintf(stderr, "File length isn't a multiple of the word size\n");
+         return false;
+      }
+      clc->size = len;
+
+      clc->data = mmap(NULL, len, PROT_READ, MAP_PRIVATE, clc->fd, 0);
+      if (clc->data == MAP_FAILED) {
+         fprintf(stderr, "Failed to mmap libclc SPIR-V: %m\n");
+         return false;
+      }
+
+      return true;
+   }
+#endif
+
+   return true;
+}
+
+static void
+close_clc_data(struct clc_data *clc)
+{
+   if (clc->file->static_data) {
+#ifdef HAVE_STATIC_LIBCLC_ZSTD
+      free((void *)clc->data);
+#endif
+      return;
+   }
+
+#ifdef DYNAMIC_LIBCLC_PATH
+   if (clc->file->sys_path != NULL) {
+      if (clc->data)
+         munmap((void *)clc->data, clc->size);
+      close(clc->fd);
+   }
+#endif
+}
+
+/** Returns true if libclc is found
+ *
+ * If libclc is compiled in statically, this always returns true.  If we
+ * depend on a dynamic libclc, this opens and tries to stat the file.
+ */
+bool
+nir_can_find_libclc(unsigned ptr_bit_size)
+{
+   struct clc_data clc;
+   if (open_clc_data(&clc, ptr_bit_size)) {
+      close_clc_data(&clc);
+      return true;
+   } else {
+      return false;
+   }
+}
+
+nir_shader *
+nir_load_libclc_shader(unsigned ptr_bit_size,
+                       struct disk_cache *disk_cache,
+                       const struct spirv_to_nir_options *spirv_options,
+                       const nir_shader_compiler_options *nir_options)
+{
+   assert(ptr_bit_size ==
+          nir_address_format_bit_size(spirv_options->global_addr_format));
+
+   struct clc_data clc;
+   if (!open_clc_data(&clc, ptr_bit_size))
+      return NULL;
+
+#ifdef ENABLE_SHADER_CACHE
+   cache_key cache_key;
+   if (disk_cache) {
+      disk_cache_compute_key(disk_cache, clc.cache_key,
+                             sizeof(clc.cache_key), cache_key);
+
+      size_t buffer_size;
+      uint8_t *buffer = disk_cache_get(disk_cache, cache_key, &buffer_size);
+      if (buffer) {
+         struct blob_reader blob;
+         blob_reader_init(&blob, buffer, buffer_size);
+         nir_shader *nir = nir_deserialize(NULL, nir_options, &blob);
+         free(buffer);
+         close_clc_data(&clc);
+         return nir;
+      }
+   }
+#endif
+
+   if (!map_clc_data(&clc)) {
+      close_clc_data(&clc);
+      return NULL;
+   }
+
+   struct spirv_to_nir_options spirv_lib_options = *spirv_options;
+   spirv_lib_options.create_library = true;
+
+   assert(clc.size % SPIRV_WORD_SIZE == 0);
+   nir_shader *nir = spirv_to_nir(clc.data, clc.size / SPIRV_WORD_SIZE,
+                                  NULL, 0, MESA_SHADER_KERNEL, NULL,
+                                  &spirv_lib_options, nir_options);
+   nir_validate_shader(nir, "after nir_load_clc_shader");
+
+   /* nir_inline_libclc will assume that the functions in this shader are
+    * already ready to lower.  This means we need to inline any function_temp
+    * initializers and lower any early returns.
+    */
+   NIR_PASS_V(nir, nir_lower_variable_initializers, nir_var_function_temp);
+   NIR_PASS_V(nir, nir_lower_returns);
+
+   /* TODO: One day, we may want to run some optimizations on the libclc
+    * shader once and cache them to save time in each shader call.
+    */
+
+#ifdef ENABLE_SHADER_CACHE
+   if (disk_cache) {
+      struct blob blob;
+      blob_init(&blob);
+      nir_serialize(&blob, nir, false);
+      disk_cache_put(disk_cache, cache_key, blob.data, blob.size, NULL);
+   }
+#endif
+
+   close_clc_data(&clc);
+   return nir;
+}
diff --git a/src/compiler/spirv/nir_spirv.h b/src/compiler/spirv/nir_spirv.h
index 576e4a7..49921fa 100644
--- a/src/compiler/spirv/nir_spirv.h
+++ b/src/compiler/spirv/nir_spirv.h
@@ -28,6 +28,7 @@
 #ifndef _NIR_SPIRV_H_
 #define _NIR_SPIRV_H_
 
+#include "util/disk_cache.h"
 #include "compiler/nir/nir.h"
 #include "compiler/shader_info.h"
 
@@ -102,6 +103,14 @@
                          const struct spirv_to_nir_options *options,
                          const nir_shader_compiler_options *nir_options);
 
+bool nir_can_find_libclc(unsigned ptr_bit_size);
+
+nir_shader *
+nir_load_libclc_shader(unsigned ptr_bit_size,
+                       struct disk_cache *disk_cache,
+                       const struct spirv_to_nir_options *spirv_options,
+                       const nir_shader_compiler_options *nir_options);
+
 #ifdef __cplusplus
 }
 #endif