Browse Source

Squash merge webgl-port branch into master

Adds support for emscripten as a build target / platform, and adds a WebGL renderer back-end
rdb 1 year ago
parent
commit
84ed141b2a
84 changed files with 4775 additions and 172 deletions
  1. 30 0
      .github/workflows/ci.yml
  2. 12 1
      dtool/Config.cmake
  3. 3 0
      dtool/Package.cmake
  4. 19 0
      dtool/src/dtoolbase/dtool_platform.h
  5. 5 1
      dtool/src/dtoolbase/memoryHook.I
  6. 10 2
      dtool/src/dtoolbase/memoryHook.cxx
  7. 10 1
      dtool/src/dtoolbase/memoryHook.h
  8. 6 3
      dtool/src/dtoolutil/executionEnvironment.cxx
  9. 3 3
      dtool/src/dtoolutil/filename.cxx
  10. 4 0
      dtool/src/dtoolutil/pfstream.h
  11. 4 0
      dtool/src/dtoolutil/pfstreamBuf.cxx
  12. 5 0
      dtool/src/dtoolutil/pfstreamBuf.h
  13. 1 0
      dtool/src/parser-inc/emscripten.h
  14. 13 0
      dtool/src/parser-inc/emscripten/em_asm.h
  15. 3 0
      dtool/src/parser-inc/emscripten/em_js.h
  16. 24 0
      dtool/src/parser-inc/emscripten/emscripten.h
  17. 7 0
      dtool/src/parser-inc/emscripten/fiber.h
  18. 0 0
      dtool/src/parser-inc/emscripten/trace.h
  19. 5 0
      dtool/src/prc/CMakeLists.txt
  20. 0 19
      dtool/src/prc/androidLogStream.cxx
  21. 2 0
      dtool/src/prc/androidLogStream.h
  22. 23 1
      dtool/src/prc/configPageManager.cxx
  23. 111 0
      dtool/src/prc/emscriptenLogStream.cxx
  24. 57 0
      dtool/src/prc/emscriptenLogStream.h
  25. 66 1
      dtool/src/prc/notify.cxx
  26. 4 25
      dtool/src/prc/notifyCategory.cxx
  27. 1 0
      dtool/src/prc/p3prc_composite2.cxx
  28. 13 0
      dtool/src/prc/pnotify.h
  29. 194 68
      makepanda/makepanda.py
  30. 68 16
      makepanda/makepandacore.py
  31. 1 1
      panda/src/downloader/httpAuthorization.cxx
  32. 1 1
      panda/src/downloader/httpAuthorization.h
  33. 1 1
      panda/src/downloader/httpBasicAuthorization.cxx
  34. 1 1
      panda/src/downloader/httpBasicAuthorization.h
  35. 3 0
      panda/src/downloader/httpChannel.h
  36. 442 0
      panda/src/downloader/httpChannel_emscripten.I
  37. 790 0
      panda/src/downloader/httpChannel_emscripten.cxx
  38. 241 0
      panda/src/downloader/httpChannel_emscripten.h
  39. 4 0
      panda/src/downloader/httpClient.h
  40. 32 0
      panda/src/downloader/httpClient_emscripten.I
  41. 424 0
      panda/src/downloader/httpClient_emscripten.cxx
  42. 93 0
      panda/src/downloader/httpClient_emscripten.h
  43. 1 1
      panda/src/downloader/httpCookie.cxx
  44. 1 1
      panda/src/downloader/httpCookie.h
  45. 1 1
      panda/src/downloader/httpDigestAuthorization.cxx
  46. 1 1
      panda/src/downloader/httpDigestAuthorization.h
  47. 1 1
      panda/src/downloader/httpEnum.cxx
  48. 3 1
      panda/src/downloader/httpEnum.h
  49. 2 0
      panda/src/downloader/p3downloader_composite2.cxx
  50. 24 1
      panda/src/downloader/virtualFileHTTP.cxx
  51. 2 1
      panda/src/downloader/virtualFileHTTP.h
  52. 1 1
      panda/src/downloader/virtualFileMountHTTP.cxx
  53. 1 1
      panda/src/downloader/virtualFileMountHTTP.h
  54. 13 0
      panda/src/event/asyncTaskManager.cxx
  55. 40 0
      panda/src/express/trueClock.cxx
  56. 1 1
      panda/src/express/virtualFile.h
  57. 51 3
      panda/src/express/virtualFileSystem.cxx
  58. 21 1
      panda/src/framework/pandaFramework.cxx
  59. 8 0
      panda/src/glstuff/glGraphicsStateGuardian_src.I
  60. 34 5
      panda/src/glstuff/glGraphicsStateGuardian_src.cxx
  61. 3 0
      panda/src/glstuff/glGraphicsStateGuardian_src.h
  62. 4 0
      panda/src/glstuff/glShaderContext_src.cxx
  63. 13 0
      panda/src/gobj/texturePool.cxx
  64. 3 1
      panda/src/nativenet/socket_portable.h
  65. 2 0
      panda/src/net/connectionManager.cxx
  66. 2 0
      panda/src/pnmimage/CMakeLists.txt
  67. 1 0
      panda/src/pnmimage/p3pnmimage_composite2.cxx
  68. 56 5
      panda/src/pnmimage/pnmImageHeader.cxx
  69. 73 0
      panda/src/pnmimage/pnmReaderEmscripten.cxx
  70. 40 0
      panda/src/pnmimage/pnmReaderEmscripten.h
  71. 1 1
      panda/src/pnmimagetypes/config_pnmimagetypes.h
  72. 53 0
      panda/src/webgldisplay/config_webgldisplay.cxx
  73. 27 0
      panda/src/webgldisplay/config_webgldisplay.h
  74. 4 0
      panda/src/webgldisplay/p3webgldisplay_composite1.cxx
  75. 12 0
      panda/src/webgldisplay/webGLGraphicsPipe.I
  76. 138 0
      panda/src/webgldisplay/webGLGraphicsPipe.cxx
  77. 71 0
      panda/src/webgldisplay/webGLGraphicsPipe.h
  78. 12 0
      panda/src/webgldisplay/webGLGraphicsStateGuardian.I
  79. 174 0
      panda/src/webgldisplay/webGLGraphicsStateGuardian.cxx
  80. 72 0
      panda/src/webgldisplay/webGLGraphicsStateGuardian.h
  81. 12 0
      panda/src/webgldisplay/webGLGraphicsWindow.I
  82. 971 0
      panda/src/webgldisplay/webGLGraphicsWindow.cxx
  83. 87 0
      panda/src/webgldisplay/webGLGraphicsWindow.h
  84. 2 0
      tests/putil/test_datagram.py

+ 30 - 0
.github/workflows/ci.yml

@@ -460,3 +460,33 @@ jobs:
     - name: Make installer
       run: |
         python makepanda/makepackage.py --verbose --lzma
+
+  emscripten:
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/checkout@v1
+
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install build-essential ninja-build bison flex
+
+    - name: Build Host Interrogate
+      shell: bash
+      run: |
+        mkdir -p host-build
+        cmake -S . -B host-build -DBUILD_DIRECT=OFF -DBUILD_PANDA=OFF -DBUILD_PANDATOOL=OFF -DBUILD_CONTRIB=OFF -DBUILD_DTOOL=ON -DBUILD_MODELS=OFF -DBUILD_SHARED_LIBS=OFF -DINTERROGATE_PYTHON_INTERFACE=OFF
+        cmake --build host-build --config Standard --parallel 4
+        echo host-build/bin >> $GITHUB_PATH
+
+    - name: Setup emsdk
+      uses: mymindstorm/setup-emsdk@v14
+      with:
+        version: 3.1.51
+        actions-cache-folder: 'emsdk-cache'
+
+    - name: Build for Emscripten
+      shell: bash
+      run: |
+        python3 makepanda/makepanda.py --git-commit=${{github.sha}} --target emscripten --outputdir=built --everything --no-python --verbose --threads=4

+ 12 - 1
dtool/Config.cmake

@@ -30,10 +30,15 @@ if(DEFINED CMAKE_CXX_FLAGS_COVERAGE)
 endif()
 
 # Are we building with static or dynamic linking?
+if(EMSCRIPTEN OR WASI)
+  set(_default_shared OFF)
+else()
+  set(_default_shared ON)
+endif()
 option(BUILD_SHARED_LIBS
   "Causes subpackages to be built separately -- setup for dynamic linking.
 Utilities/tools/binaries/etc are then dynamically linked to the
-libraries instead of being statically linked." ON)
+libraries instead of being statically linked." ${_default_shared})
 
 option(BUILD_METALIBS
   "Should we build 'metalibs' -- fewer, larger libraries that contain the bulk
@@ -561,12 +566,18 @@ set(THREADS_LIBRARIES "${CMAKE_THREAD_LIBS_INIT}")
 set(HAVE_POSIX_THREADS ${CMAKE_USE_PTHREADS_INIT})
 
 # Add basic use flag for threading
+if(EMSCRIPTEN OR WASI)
+  set(_default_threads OFF)
+else()
+  set(_default_threads ON)
+endif()
 package_option(THREADS
   "If on, compile Panda3D with threading support.
 Building in support for threading will enable Panda to take
 advantage of multiple CPU's if you have them (and if the OS
 supports kernel threads running on different CPU's), but it will
 slightly slow down Panda for the single CPU case."
+  DEFAULT ${_default_threads}
   IMPORTED_AS Threads::Threads)
 
 # Configure debug threads

+ 3 - 0
dtool/Package.cmake

@@ -2,6 +2,9 @@ set(_thirdparty_dir_default "${PROJECT_SOURCE_DIR}/thirdparty")
 if(NOT IS_DIRECTORY "${_thirdparty_dir_default}")
   set(_thirdparty_dir_default "")
 endif()
+if(CMAKE_SYSTEM_NAME STREQUAL "WASI")
+  set(_thirdparty_dir_default "")
+endif()
 
 set(THIRDPARTY_DIRECTORY "${_thirdparty_dir_default}" CACHE PATH
   "Optional location of a makepanda-style thirdparty directory. All libraries

+ 19 - 0
dtool/src/dtoolbase/dtool_platform.h

@@ -65,6 +65,25 @@
 #define DTOOL_PLATFORM "android_i386"
 #endif
 
+#elif defined(__EMSCRIPTEN__)
+#if defined(__wasm64__)
+#define DTOOL_PLATFORM "emscripten_wasm64"
+#elif defined(__wasm32__)
+#define DTOOL_PLATFORM "emscripten_wasm32"
+#else
+#define DTOOL_PLATFORM "emscripten"
+#endif
+
+#elif defined(__wasm32__)
+#define DTOOL_PLATFORM "wasi_wasm32"
+
+#elif defined(__wasm64__)
+#define DTOOL_PLATFORM "wasi_wasm64"
+
+// fallback to wasm32 with older sdk
+#elif defined(__wasi__)
+#define DTOOL_PLATFORM "wasi_wasm32"
+
 #elif defined(__aarch64__)
 #define DTOOL_PLATFORM "linux_aarch64"
 

+ 5 - 1
dtool/src/dtoolbase/memoryHook.I

@@ -40,9 +40,11 @@ dec_heap(size_t size) {
  */
 INLINE size_t MemoryHook::
 get_page_size() const {
+#ifndef __EMSCRIPTEN__
   if (_page_size == 0) {
     determine_page_size();
   }
+#endif
   return _page_size;
 }
 
@@ -52,10 +54,12 @@ get_page_size() const {
  */
 INLINE size_t MemoryHook::
 round_up_to_page_size(size_t size) const {
+#ifndef __EMSCRIPTEN__
   if (_page_size == 0) {
     determine_page_size();
   }
-  return  ((size + _page_size - 1) / _page_size) * _page_size;
+#endif
+  return ((size + _page_size - 1) / _page_size) * _page_size;
 }
 
 /**

+ 10 - 2
dtool/src/dtoolbase/memoryHook.cxx

@@ -207,8 +207,12 @@ MemoryHook(const MemoryHook &copy) :
   _total_heap_array_size(copy._total_heap_array_size.load(std::memory_order_relaxed)),
   _requested_heap_size(copy._requested_heap_size.load(std::memory_order_relaxed)),
   _total_mmap_size(copy._total_mmap_size.load(std::memory_order_relaxed)),
-  _max_heap_size(copy._max_heap_size),
-  _page_size(copy._page_size) {
+  _max_heap_size(copy._max_heap_size)
+#ifndef __EMSCRIPTEN__
+  ,
+  _page_size(copy._page_size)
+#endif
+{
 }
 
 
@@ -483,9 +487,11 @@ heap_trim(size_t pad) {
  */
 void *MemoryHook::
 mmap_alloc(size_t size, bool allow_exec) {
+#ifndef __EMSCRIPTEN__
   if (_page_size == 0) {
     determine_page_size();
   }
+#endif
   assert((size % _page_size) == 0);
 
 #ifdef DO_MEMORY_USAGE
@@ -602,6 +608,7 @@ overflow_heap_size() {
 /**
  * Asks the operating system for the page size.
  */
+#ifndef __EMSCRIPTEN__
 void MemoryHook::
 determine_page_size() const {
 #ifdef _WIN32
@@ -619,3 +626,4 @@ determine_page_size() const {
 
   assert(_page_size != 0);
 }
+#endif  // !__EMSCRIPTEN__

+ 10 - 1
dtool/src/dtoolbase/memoryHook.h

@@ -20,6 +20,10 @@
 #include "mutexImpl.h"
 #include <map>
 
+#ifdef __EMSCRIPTEN__
+#include <emscripten/heap.h>
+#endif
+
 /**
  * This class provides a wrapper around the various possible malloc schemes
  * Panda might employ.  It also exists to allow the MemoryUsage class in Panda
@@ -77,12 +81,17 @@ protected:
 
   virtual void overflow_heap_size();
 
+#ifndef __EMSCRIPTEN__
   void determine_page_size() const;
+#endif
 
 private:
+#ifdef __EMSCRIPTEN__
+  static const size_t _page_size = EMSCRIPTEN_PAGE_SIZE;
+#else
   mutable size_t _page_size = 0;
-
   mutable MutexImpl _lock;
+#endif
 };
 
 #include "memoryHook.I"

+ 6 - 3
dtool/src/dtoolutil/executionEnvironment.cxx

@@ -62,7 +62,7 @@ extern char **environ;
 #include <sys/sysctl.h>
 #endif
 
-#if defined(IS_LINUX) || defined(IS_FREEBSD)
+#if (defined(IS_LINUX) || defined(IS_FREEBSD)) && !defined(__wasi__)
 // For link_map and dlinfo.
 #include <link.h>
 #include <dlfcn.h>
@@ -576,6 +576,9 @@ read_environment_variables() {
       _variables[variable] = value;
     }
   }
+#elif defined(__EMSCRIPTEN__)
+  // Emscripten has no environment vars.  Don't even try.
+
 #elif defined(HAVE_PROC_SELF_ENVIRON)
   // In some cases, we may have a file called procselfenviron that may be read
   // to determine all of our environment variables.
@@ -918,7 +921,7 @@ read_args() {
   }
 #endif
 
-#ifndef _WIN32
+#if !defined(_WIN32) && !defined(__wasi__)
   // Try to use realpath to get cleaner paths.
 
   if (!_binary_name.empty()) {
@@ -934,7 +937,7 @@ read_args() {
       _dtool_name = newpath;
     }
   }
-#endif  // _WIN32
+#endif  // _WIN32 __wasi__
 
   if (_dtool_name.empty()) {
     _dtool_name = _binary_name;

+ 3 - 3
dtool/src/dtoolutil/filename.cxx

@@ -436,7 +436,7 @@ Filename Filename::
 temporary(const string &dirname, const string &prefix, const string &suffix,
           Type type) {
   Filename fdirname = dirname;
-#if defined(_WIN32) || defined(ANDROID)
+#if defined(_WIN32) || defined(ANDROID) || defined(__wasi__)
   // The Windows tempnam() function doesn't do a good job of choosing a
   // temporary directory.  Choose one ourselves.
   if (fdirname.empty()) {
@@ -1049,7 +1049,7 @@ make_canonical() {
     return true;
   }
 
-#ifndef _WIN32
+#if !defined(_WIN32) && !defined(__wasi__)
   // Use realpath in order to resolve symlinks properly
   char newpath [PATH_MAX + 1];
   if (realpath(c_str(), newpath) != nullptr) {
@@ -1057,7 +1057,7 @@ make_canonical() {
     newpath_fn._flags = _flags;
     (*this) = newpath_fn;
   }
-#endif
+#endif  // _WIN32 __wasi__
 
   Filename cwd = ExecutionEnvironment::get_cwd();
   if (!r_make_canonical(cwd)) {

+ 4 - 0
dtool/src/dtoolutil/pfstream.h

@@ -16,6 +16,8 @@
 
 #include "pfstreamBuf.h"
 
+#ifndef __EMSCRIPTEN__
+
 class EXPCL_DTOOL_DTOOLUTIL IPipeStream : public std::istream {
 public:
   INLINE IPipeStream(const std::string);
@@ -50,4 +52,6 @@ private:
 
 #include "pfstream.I"
 
+#endif /* __EMSCRIPTEN__ */
+
 #endif /* __PFSTREAM_H__ */

+ 4 - 0
dtool/src/dtoolutil/pfstreamBuf.cxx

@@ -14,6 +14,8 @@
 #include "pfstreamBuf.h"
 #include <assert.h>
 
+#ifndef __EMSCRIPTEN__
+
 using std::cerr;
 using std::endl;
 using std::string;
@@ -404,3 +406,5 @@ read_pipe(char *data, size_t len) {
 
 
 #endif  // WIN_PIPE_CALLS
+
+#endif  // __EMSCRIPTEN__

+ 5 - 0
dtool/src/dtoolutil/pfstreamBuf.h

@@ -18,6 +18,9 @@
 #include <string>
 #include <stdio.h>
 
+// Emscripten does not have popen.
+#ifndef __EMSCRIPTEN__
+
 // By default, we'll use the Windows flavor of pipe functions if we're
 // compiling under Windows.  Turn this off to use popen(), even on Windows.
 // (popen() doesn't seem to work on Win9x, although it does work on NT-based
@@ -77,4 +80,6 @@ private:
   void write_chars(const char*, int, bool);
 };
 
+#endif /* __EMSCRIPTEN__ */
+
 #endif /* __PFSTREAMBUF_H__ */

+ 1 - 0
dtool/src/parser-inc/emscripten.h

@@ -0,0 +1 @@
+#include <emscripten/emscripten.h>

+ 13 - 0
dtool/src/parser-inc/emscripten/em_asm.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#define EM_ASM(...)
+#define EM_ASM_INT(...)
+#define EM_ASM_DOUBLE(...)
+#define MAIN_THREAD_EM_ASM(...)
+#define MAIN_THREAD_EM_ASM_INT(...)
+#define MAIN_THREAD_EM_ASM_DOUBLE(...)
+#define MAIN_THREAD_ASYNC_EM_ASM(...)
+#define EM_ASM_(...)
+#define EM_ASM_ARGS(...)
+#define EM_ASM_INT_V(...)
+#define EM_ASM_DOUBLE_V(...)

+ 3 - 0
dtool/src/parser-inc/emscripten/em_js.h

@@ -0,0 +1,3 @@
+#pragma once
+
+#define EM_JS(ret, name, params, ...)

+ 24 - 0
dtool/src/parser-inc/emscripten/emscripten.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include "em_asm.h"
+#include "em_js.h"
+
+typedef short emscripten_align1_short;
+
+typedef long long emscripten_align4_int64;
+typedef long long emscripten_align2_int64;
+typedef long long emscripten_align1_int64;
+
+typedef int emscripten_align2_int;
+typedef int emscripten_align1_int;
+
+typedef float emscripten_align2_float;
+typedef float emscripten_align1_float;
+
+typedef double emscripten_align4_double;
+typedef double emscripten_align2_double;
+typedef double emscripten_align1_double;
+
+typedef void (*em_callback_func)(void);
+typedef void (*em_arg_callback_func)(void*);
+typedef void (*em_str_callback_func)(const char *);

+ 7 - 0
dtool/src/parser-inc/emscripten/fiber.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#include <stdint.h>
+#include <emscripten/emscripten.h>
+
+typedef struct asyncify_data_s asyncify_data_t;
+typedef struct emscripten_fiber_s emscripten_fiber_t;

+ 0 - 0
dtool/src/parser-inc/emscripten/trace.h


+ 5 - 0
dtool/src/prc/CMakeLists.txt

@@ -69,6 +69,11 @@ if(ANDROID)
     androidLogStream.cxx)
 endif()
 
+if(EMSCRIPTEN)
+  set(P3PRC_SOURCES ${P3PRC_SOURCES}
+    emscriptenLogStream.cxx)
+endif()
+
 if(HAVE_OPENSSL)
   list(APPEND P3PRC_SOURCES encryptStreamBuf.cxx encryptStream.cxx)
 endif()

+ 0 - 19
dtool/src/prc/androidLogStream.cxx

@@ -118,23 +118,4 @@ AndroidLogStream::
   delete rdbuf();
 }
 
-/**
- * Returns an AndroidLogStream suitable for writing log messages with the
- * indicated severity.
- */
-std::ostream &AndroidLogStream::
-out(NotifySeverity severity) {
-  static AndroidLogStream* streams[NS_fatal + 1] = {nullptr};
-
-  if (streams[severity] == nullptr) {
-    int priority = ANDROID_LOG_UNKNOWN;
-    if (severity != NS_unspecified) {
-      priority = ((int)severity) + 1;
-    }
-    streams[severity] = new AndroidLogStream(priority);
-  }
-
-  return *streams[severity];
-}
-
 #endif  // ANDROID

+ 2 - 0
dtool/src/prc/androidLogStream.h

@@ -49,6 +49,8 @@ public:
   virtual ~AndroidLogStream();
 
   static std::ostream &out(NotifySeverity severity);
+
+  friend class Notify;
 };
 
 #endif  // ANDROID

+ 23 - 1
dtool/src/prc/configPageManager.cxx

@@ -127,11 +127,13 @@ reload_implicit_pages() {
   const BlobInfo *blobinfo = (const BlobInfo *)dlsym(RTLD_MAIN_ONLY, "blobinfo");
 //#elif defined(RTLD_SELF)
 //  const BlobInfo *blobinfo = (const BlobInfo *)dlsym(RTLD_SELF, "blobinfo");
+#elif defined(__EMSCRIPTEN__)
+  const BlobInfo *blobinfo = nullptr;
 #else
   const BlobInfo *blobinfo = (const BlobInfo *)dlsym(dlopen(nullptr, RTLD_NOW), "blobinfo");
 #endif
   if (blobinfo == nullptr) {
-#ifndef _WIN32
+#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
     // Clear the error flag.
     dlerror();
 #endif
@@ -153,6 +155,7 @@ reload_implicit_pages() {
   // Pull them out and store them in _prc_patterns.
   _prc_patterns.clear();
 
+#ifdef PRC_PATTERNS
   string prc_patterns = PRC_PATTERNS;
   if (blobinfo != nullptr && blobinfo->prc_patterns != nullptr) {
     prc_patterns = blobinfo->prc_patterns;
@@ -171,10 +174,12 @@ reload_implicit_pages() {
       _prc_patterns.push_back(glob);
     }
   }
+#endif  // PRC_PATTERNS
 
   // Similarly for PRC_ENCRYPTED_PATTERNS.
   _prc_encrypted_patterns.clear();
 
+#ifdef PRC_ENCRYPTED_PATTERNS
   string prc_encrypted_patterns = PRC_ENCRYPTED_PATTERNS;
   if (blobinfo != nullptr && blobinfo->prc_encrypted_patterns != nullptr) {
     prc_encrypted_patterns = blobinfo->prc_encrypted_patterns;
@@ -191,10 +196,12 @@ reload_implicit_pages() {
       _prc_encrypted_patterns.push_back(glob);
     }
   }
+#endif  // PRC_ENCRYPTED_PATTERNS
 
   // And again for PRC_EXECUTABLE_PATTERNS.
   _prc_executable_patterns.clear();
 
+#ifdef PRC_EXECUTABLE_PATTERNS
   string prc_executable_patterns = PRC_EXECUTABLE_PATTERNS;
   if (blobinfo != nullptr && blobinfo->prc_executable_patterns != nullptr) {
     prc_executable_patterns = blobinfo->prc_executable_patterns;
@@ -211,10 +218,12 @@ reload_implicit_pages() {
       _prc_executable_patterns.push_back(glob);
     }
   }
+#endif  // PRC_EXECUTABLE_PATTERNS
 
   // Now build up the search path for .prc files.
   _search_path.clear();
 
+#ifdef PRC_DIR_ENVVARS
   // PRC_DIR_ENVVARS lists one or more environment variables separated by
   // spaces.  Pull them out, and each of those contains the name of a single
   // directory to search.  Add it to the search path.
@@ -236,7 +245,9 @@ reload_implicit_pages() {
       }
     }
   }
+#endif  // PRC_DIR_ENVVARS
 
+#ifdef PRC_PATH_ENVVARS
   // PRC_PATH_ENVVARS lists one or more environment variables separated by
   // spaces.  Pull them out, and then each one of those contains a list of
   // directories to search.  Add each of those to the search path.
@@ -264,7 +275,9 @@ reload_implicit_pages() {
       }
     }
   }
+#endif  // PRC_PATH_ENVVARS
 
+#ifdef PRC_PATH2_ENVVARS
 /*
  * PRC_PATH2_ENVVARS is a special variable that is rarely used; it exists
  * primarily to support the Cygwin-based "ctattach" tools used by the Walt
@@ -294,7 +307,9 @@ reload_implicit_pages() {
       }
     }
   }
+#endif  // PRC_PATH2_ENVVARS
 
+#ifdef DEFAULT_PRC_DIR
   if (_search_path.is_empty()) {
     // If nothing's on the search path (PRC_DIR and PRC_PATH were not
     // defined), then use the DEFAULT_PRC_DIR.
@@ -310,6 +325,7 @@ reload_implicit_pages() {
       }
     }
   }
+#endif  // DEFAULT_PRC_DIR
 
   // Now find all of the *.prc files (or whatever matches PRC_PATTERNS) on the
   // path.
@@ -398,6 +414,11 @@ reload_implicit_pages() {
 
     if ((file._file_flags & FF_execute) != 0 &&
         filename.is_executable()) {
+
+#ifdef __EMSCRIPTEN__
+      prc_cat.error()
+        << "Executable config files are not supported with Emscripten.\n";
+#else
       // Attempt to execute the file as a command.
       string command = filename.to_os_specific();
 
@@ -420,6 +441,7 @@ reload_implicit_pages() {
       _pages_sorted = false;
 
       page->read_prc(ifs);
+#endif  // __EMSCRIPTEN__
 
     } else if ((file._file_flags & FF_decrypt) != 0) {
       // Read and decrypt the file.

+ 111 - 0
dtool/src/prc/emscriptenLogStream.cxx

@@ -0,0 +1,111 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file emscriptenLogStream.cxx
+ * @author rdb
+ * @date 2015-04-02
+ */
+
+#include "emscriptenLogStream.h"
+#include "configVariableString.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include <emscripten.h>
+
+/**
+ *
+ */
+EmscriptenLogStream::EmscriptenLogStreamBuf::
+EmscriptenLogStreamBuf(int flags) :
+  _flags(flags) {
+
+  // The EmscriptenLogStreamBuf doesn't actually need a buffer--it's happy
+  // writing characters one at a time, since they're just getting stuffed into
+  // a string.  (Although the code is written portably enough to use a buffer
+  // correctly, if we had one.)
+  setg(0, 0, 0);
+  setp(0, 0);
+}
+
+/**
+ *
+ */
+EmscriptenLogStream::EmscriptenLogStreamBuf::
+~EmscriptenLogStreamBuf() {
+  sync();
+}
+
+/**
+ * Called by the system ostream implementation when the buffer should be
+ * flushed to output (for instance, on destruction).
+ */
+int EmscriptenLogStream::EmscriptenLogStreamBuf::
+sync() {
+  std::streamsize n = pptr() - pbase();
+
+  // Write the characters that remain in the buffer.
+  for (char *p = pbase(); p < pptr(); ++p) {
+    write_char(*p);
+  }
+
+  pbump(-n);  // Reset pptr().
+  return 0;  // EOF to indicate write full.
+}
+
+/**
+ * Called by the system ostream implementation when its internal buffer is
+ * filled, plus one character.
+ */
+int EmscriptenLogStream::EmscriptenLogStreamBuf::
+overflow(int ch) {
+  std::streamsize n = pptr() - pbase();
+
+  if (n != 0 && sync() != 0) {
+    return EOF;
+  }
+
+  if (ch != EOF) {
+    // Write one more character.
+    write_char(ch);
+  }
+
+  return 0;
+}
+
+/**
+ * Stores a single character.
+ */
+void EmscriptenLogStream::EmscriptenLogStreamBuf::
+write_char(char c) {
+  if (c == '\n') {
+    // Write a line to the log file.
+    emscripten_log(_flags, "%.*s", _data.size(), _data.c_str());
+    _data.clear();
+  } else {
+    _data += c;
+  }
+}
+
+/**
+ *
+ */
+EmscriptenLogStream::
+EmscriptenLogStream(int flags) :
+  std::ostream(new EmscriptenLogStreamBuf(flags)) {
+}
+
+/**
+ *
+ */
+EmscriptenLogStream::
+~EmscriptenLogStream() {
+  delete rdbuf();
+}
+
+#endif  // __EMSCRIPTEN__

+ 57 - 0
dtool/src/prc/emscriptenLogStream.h

@@ -0,0 +1,57 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file emscriptenLogStream.h
+ * @author rdb
+ * @date 2015-04-02
+ */
+
+#ifndef EMSCRIPTENLOGSTREAM_H
+#define EMSCRIPTENLOGSTREAM_H
+
+#ifdef __EMSCRIPTEN__
+
+#include "dtoolbase.h"
+#include "notifySeverity.h"
+
+#include <string>
+#include <iostream>
+
+/**
+ * This is a type of ostream that writes each line to the JavaScript log
+ * window.
+ */
+class EmscriptenLogStream : public std::ostream {
+private:
+  class EmscriptenLogStreamBuf : public std::streambuf {
+  public:
+    EmscriptenLogStreamBuf(int flags);
+    virtual ~EmscriptenLogStreamBuf();
+
+  protected:
+    virtual int overflow(int c);
+    virtual int sync();
+
+  private:
+    void write_char(char c);
+
+    int _flags;
+    string _data;
+  };
+
+  EmscriptenLogStream(int flags);
+
+public:
+  virtual ~EmscriptenLogStream();
+
+  friend class Notify;
+};
+
+#endif  // __EMSCRIPTEN__
+
+#endif  // EMSCRIPTENLOGSTREAM_H

+ 66 - 1
dtool/src/prc/notify.cxx

@@ -28,10 +28,13 @@
 
 #ifdef ANDROID
 #include <android/log.h>
-
 #include "androidLogStream.h"
 #endif
 
+#ifdef __EMSCRIPTEN__
+#include "emscriptenLogStream.h"
+#endif
+
 using std::cerr;
 using std::cout;
 using std::ostream;
@@ -255,6 +258,21 @@ get_category(const string &fullname) {
   return get_category(basename, parent_category);
 }
 
+/**
+ * A convenient way to get the ostream that should be written to for a Notify-
+ * type message of a particular severity.  Also see Category::out() for a
+ * message that is specific to a particular Category.
+ */
+ostream &Notify::
+out(NotifySeverity severity) {
+#if defined(ANDROID) || defined(__EMSCRIPTEN__)
+  // Android and JavaScript have dedicated log systems.
+  return *(ptr()->_log_streams[severity]);
+#else
+  return *(ptr()->_ostream_ptr);
+#endif
+}
+
 /**
  * A convenient way to get the ostream that should be written to for a Notify-
  * type message.  Also see Category::out() for a message that is specific to a
@@ -342,7 +360,15 @@ assert_failure(const char *expression, int line,
   }
 
 #ifdef ANDROID
+  // Write to Android log system.
   __android_log_assert("assert", "Panda3D", "Assertion failed: %s", message.c_str());
+#endif
+
+#ifdef __EMSCRIPTEN__
+  // Write to JavaScript console.
+  emscripten_log(EM_LOG_CONSOLE | EM_LOG_ERROR | EM_LOG_C_STACK,
+                 "Assertion failed: %s", message.c_str());
+
 #else
   nout << "Assertion failed: " << message << "\n";
 #endif
@@ -372,6 +398,11 @@ assert_failure(const char *expression, int line,
     int *ptr = nullptr;
     *ptr = 1;
 
+#elif defined(__EMSCRIPTEN__)
+    // This should drop us into the browser's JavaScript debugger.
+    //emscripten_debugger();
+    EM_ASM(debugger;);
+
 #else  // _MSC_VER
     abort();
 #endif  // _MSC_VER
@@ -429,6 +460,39 @@ config_initialized() {
   // notify-output even after the initial import of Panda3D modules.  However,
   // it cannot be changed after the first time it is set.
 
+#if defined(ANDROID)
+  // Android redirects stdio and stderr to /dev/null,
+  // but does provide its own logging system.  We use a special
+  // type of stream that redirects it to Android's log system.
+
+  Notify *ptr = Notify::ptr();
+
+  for (int i = 0; i <= NS_fatal; ++i) {
+    int priority = ANDROID_LOG_UNKNOWN;
+    if (severity != NS_unspecified) {
+      priority = i + 1;
+    }
+    ptr->_log_streams[i] = new AndroidLogStream(priority);
+  }
+
+#elif defined(__EMSCRIPTEN__)
+  // We have no writable filesystem in JavaScript.  Instead, we set up a
+  // special stream that logs straight into the Javascript console.
+
+  EmscriptenLogStream *error_stream = new EmscriptenLogStream(EM_LOG_CONSOLE | EM_LOG_ERROR);
+  EmscriptenLogStream *warn_stream = new EmscriptenLogStream(EM_LOG_CONSOLE | EM_LOG_WARN);
+  EmscriptenLogStream *info_stream = new EmscriptenLogStream(EM_LOG_CONSOLE);
+
+  Notify *ptr = Notify::ptr();
+  ptr->_log_streams[NS_unspecified] = info_stream;
+  ptr->_log_streams[NS_spam] = info_stream;
+  ptr->_log_streams[NS_debug] = info_stream;
+  ptr->_log_streams[NS_info] = info_stream;
+  ptr->_log_streams[NS_warning] = warn_stream;
+  ptr->_log_streams[NS_error] = error_stream;
+  ptr->_log_streams[NS_fatal] = error_stream;
+
+#else
   if (_global_ptr == nullptr || _global_ptr->_ostream_ptr == &cerr) {
     static ConfigVariableFilename notify_output
       ("notify-output", "",
@@ -484,4 +548,5 @@ config_initialized() {
 #endif
     }
   }
+#endif
 }

+ 4 - 25
dtool/src/prc/notifyCategory.cxx

@@ -18,10 +18,6 @@
 #include "configVariableBool.h"
 #include "config_prc.h"
 
-#ifdef ANDROID
-#include "androidLogStream.h"
-#endif
-
 #include <time.h>  // for strftime().
 #include <assert.h>
 
@@ -58,22 +54,6 @@ NotifyCategory(const std::string &fullname, const std::string &basename,
 std::ostream &NotifyCategory::
 out(NotifySeverity severity, bool prefix) const {
   if (is_on(severity)) {
-
-#ifdef ANDROID
-    // Android redirects stdio and stderr to devnull, but does provide its own
-    // logging system.  We use a special type of stream that redirects it to
-    // Android's log system.
-    if (prefix) {
-      if (severity == NS_info) {
-        return AndroidLogStream::out(severity) << *this << ": ";
-      } else {
-        return AndroidLogStream::out(severity) << *this << "(" << severity << "): ";
-      }
-    } else {
-      return AndroidLogStream::out(severity);
-    }
-#else
-
     if (prefix) {
       if (get_notify_timestamp()) {
         // Format a timestamp to include as a prefix as well.
@@ -87,18 +67,17 @@ out(NotifySeverity severity, bool prefix) const {
 
         char buffer[128];
         strftime(buffer, 128, ":%m-%d-%Y %H:%M:%S ", &atm);
-        nout << buffer;
+        Notify::out(severity) << buffer;
       }
 
       if (severity == NS_info) {
-        return nout << *this << ": ";
+        return Notify::out(severity) << *this << ": ";
       } else {
-        return nout << *this << "(" << severity << "): ";
+        return Notify::out(severity) << *this << "(" << severity << "): ";
       }
     } else {
-      return nout;
+      return Notify::out(severity);
     }
-#endif
 
   } else if (severity <= NS_debug && get_check_debug_notify_protect()) {
     // Someone issued a debug Notify output statement without protecting it

+ 1 - 0
dtool/src/prc/p3prc_composite2.cxx

@@ -2,6 +2,7 @@
 #include "configVariableManager.cxx"
 #include "configVariableSearchPath.cxx"
 #include "configVariableString.cxx"
+#include "emscriptenLogStream.cxx"
 #include "encryptStreamBuf.cxx"
 #include "encryptStream.cxx"
 #include "nativeNumericData.cxx"

+ 13 - 0
dtool/src/prc/pnotify.h

@@ -18,7 +18,13 @@
 #include "notifySeverity.h"
 #include <map>
 
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
+
 class NotifyCategory;
+class AndroidLogStream;
+class EmscriptenLogStream;
 
 /**
  * An object that handles general error reporting to the user.  It contains a
@@ -61,6 +67,7 @@ PUBLISHED:
                                const std::string &parent_fullname);
   NotifyCategory *get_category(const std::string &fullname);
 
+  static std::ostream &out(NotifySeverity severity);
   static std::ostream &out();
   static std::ostream &null();
   static void write_string(const std::string &str);
@@ -92,6 +99,12 @@ private:
   typedef std::map<std::string, NotifyCategory *> Categories;
   Categories _categories;
 
+#if defined(ANDROID)
+  AndroidLogStream *_log_streams[NS_fatal + 1];
+#elif defined(__EMSCRIPTEN__)
+  EmscriptenLogStream *_log_streams[NS_fatal + 1];
+#endif
+
   static Notify *_global_ptr;
 };
 

+ 194 - 68
makepanda/makepanda.py

@@ -862,7 +862,6 @@ if (COMPILER=="GCC"):
     assimp_libs = ("libassimp", "libassimpd")
 
     #         Name         pkg-config   libs, include(dir)s
-    SmartPkgEnable("EIGEN",     "eigen3",    (), ("Eigen/Dense",), target_pkg = 'ALWAYS')
     SmartPkgEnable("ARTOOLKIT", "",          ("AR"), "AR/ar.h")
     SmartPkgEnable("FCOLLADA",  "",          ChooseLib(fcollada_libs, "FCOLLADA"), ("FCollada", "FCollada/FCollada.h"))
     SmartPkgEnable("ASSIMP",    "assimp",    ChooseLib(assimp_libs, "ASSIMP"), "assimp/Importer.hpp")
@@ -871,31 +870,52 @@ if (COMPILER=="GCC"):
     SmartPkgEnable("SWRESAMPLE","libswresample", "libswresample", ("libswresample/swresample.h"), target_pkg = "FFMPEG", thirdparty_dir = "ffmpeg")
     SmartPkgEnable("FFTW",      "fftw3",     ("fftw3"), ("fftw.h"))
     SmartPkgEnable("FMODEX",    "",          ("fmodex"), ("fmodex", "fmodex/fmod.h"))
-    SmartPkgEnable("FREETYPE",  "freetype2", ("freetype"), ("freetype2", "freetype2/freetype/freetype.h"))
-    SmartPkgEnable("HARFBUZZ",  "harfbuzz",  ("harfbuzz"), ("harfbuzz", "harfbuzz/hb-ft.h"))
-    SmartPkgEnable("GL",        "gl",        ("GL"), ("GL/gl.h"), framework = "OpenGL")
-    SmartPkgEnable("GLES",      "glesv1_cm", ("GLESv1_CM"), ("GLES/gl.h"), framework = "OpenGLES")
-    SmartPkgEnable("GLES2",     "glesv2",    ("GLESv2"), ("GLES2/gl2.h")) #framework = "OpenGLES"?
-    SmartPkgEnable("EGL",       "egl",       ("EGL"), ("EGL/egl.h"))
     SmartPkgEnable("NVIDIACG",  "",          ("Cg"), "Cg/cg.h", framework = "Cg")
     SmartPkgEnable("ODE",       "",          ("ode"), "ode/ode.h", tool = "ode-config")
-    SmartPkgEnable("OPENAL",    "openal",    ("openal"), "AL/al.h", framework = "OpenAL")
     SmartPkgEnable("SQUISH",    "",          ("squish"), "squish.h")
     SmartPkgEnable("TIFF",      "libtiff-4", ("tiff"), "tiff.h")
     SmartPkgEnable("VRPN",      "",          ("vrpn", "quat"), ("vrpn", "quat.h", "vrpn/vrpn_Types.h"))
-    SmartPkgEnable("BULLET", "bullet", ("BulletSoftBody", "BulletDynamics", "BulletCollision", "LinearMath"), ("bullet", "bullet/btBulletDynamicsCommon.h"))
-    SmartPkgEnable("VORBIS",    "vorbisfile",("vorbisfile", "vorbis", "ogg"), ("ogg/ogg.h", "vorbis/vorbisfile.h"))
     SmartPkgEnable("OPUS",      "opusfile",  ("opusfile", "opus", "ogg"), ("ogg/ogg.h", "opus/opusfile.h", "opus"))
     SmartPkgEnable("JPEG",      "",          ("jpeg"), "jpeglib.h")
-    SmartPkgEnable("PNG",       "libpng",    ("png"), "png.h", tool = "libpng-config")
     SmartPkgEnable("MIMALLOC",  "",          ("mimalloc"), "mimalloc.h")
 
-    # Copy freetype libraries to be specified after harfbuzz libraries as well,
-    # because there's a circular dependency between the two libraries.
-    if not PkgSkip("FREETYPE") and not PkgSkip("HARFBUZZ"):
-        for (opt, name) in LIBNAMES:
-            if opt == "FREETYPE":
-                LibName("HARFBUZZ", name)
+    if GetTarget() != 'emscripten':
+        # Most of these are provided by emscripten or via emscripten-ports.
+        SmartPkgEnable("OPENAL",   "openal",    ("openal"), "AL/al.h", framework = "OpenAL")
+        SmartPkgEnable("EIGEN",    "eigen3",    (), ("Eigen/Dense",), target_pkg = 'ALWAYS')
+        SmartPkgEnable("VORBIS",   "vorbisfile",("vorbisfile", "vorbis", "ogg"), ("ogg/ogg.h", "vorbis/vorbisfile.h"))
+        SmartPkgEnable("BULLET",   "bullet", ("BulletSoftBody", "BulletDynamics", "BulletCollision", "LinearMath"), ("bullet", "bullet/btBulletDynamicsCommon.h"))
+        SmartPkgEnable("FREETYPE", "freetype2", ("freetype"), ("freetype2", "freetype2/freetype/freetype.h"))
+        SmartPkgEnable("HARFBUZZ", "harfbuzz",  ("harfbuzz"), ("harfbuzz", "harfbuzz/hb-ft.h"))
+        SmartPkgEnable("PNG",      "libpng",    ("png"), "png.h", tool = "libpng-config")
+        SmartPkgEnable("GL",       "gl",        ("GL"), ("GL/gl.h"), framework = "OpenGL")
+        SmartPkgEnable("GLES",     "glesv1_cm", ("GLESv1_CM"), ("GLES/gl.h"), framework = "OpenGLES")
+        SmartPkgEnable("GLES2",    "glesv2",    ("GLESv2"), ("GLES2/gl2.h")) #framework = "OpenGLES"?
+        SmartPkgEnable("EGL",      "egl",       ("EGL"), ("EGL/egl.h"))
+
+        # Copy freetype libraries to be specified after harfbuzz libraries as well,
+        # because there's a circular dependency between the two libraries.
+        if not PkgSkip("FREETYPE") and not PkgSkip("HARFBUZZ"):
+            for (opt, name) in LIBNAMES:
+                if opt == "FREETYPE":
+                    LibName("HARFBUZZ", name)
+    else:
+        PkgDisable("EIGEN")
+        PkgDisable("X11")
+        PkgDisable("GL")
+        PkgDisable("GLES")
+        PkgDisable("TINYDISPLAY")
+        for pkg, empkg in {
+            'VORBIS': 'VORBIS',
+            'BULLET': 'BULLET',
+            'ZLIB': 'ZLIB',
+            'FREETYPE': 'FREETYPE',
+            'HARFBUZZ': 'HARFBUZZ',
+            'PNG': 'LIBPNG',
+        }.items():
+            if not PkgSkip(pkg):
+                LinkFlag(pkg, '-s USE_' + empkg + '=1')
+                CompileFlag(pkg, '-s USE_' + empkg + '=1')
 
     if not PkgSkip("FFMPEG"):
         if GetTarget() == "darwin":
@@ -904,8 +924,9 @@ if (COMPILER=="GCC"):
             # Needed when linking ffmpeg statically on Linux.
             LibName("FFMPEG", "-Wl,-Bsymbolic")
             # Don't export ffmpeg symbols from libp3ffmpeg when linking statically.
-            for ffmpeg_lib in ffmpeg_libs:
-                LibName("FFMPEG", "-Wl,--exclude-libs,%s.a" % (ffmpeg_lib))
+            if GetTarget() != "emscripten":
+                for ffmpeg_lib in ffmpeg_libs:
+                    LibName("FFMPEG", "-Wl,--exclude-libs,%s.a" % (ffmpeg_lib))
 
     if not PkgSkip("OPENEXR"):
         # OpenEXR libraries have different names depending on the version.
@@ -924,7 +945,7 @@ if (COMPILER=="GCC"):
             # using the OpenEXR 3 naming scheme.
             SmartPkgEnable("OPENEXR", "OpenEXR", ("OpenEXR", "IlmThread", "Imath", "Iex"), openexr_incs)
 
-    if GetTarget() != "darwin":
+    if GetTarget() not in ("darwin", "emscripten"):
         for fcollada_lib in fcollada_libs:
             LibName("FCOLLADA", "-Wl,--exclude-libs,lib%s.a" % (fcollada_lib))
 
@@ -1000,7 +1021,7 @@ if (COMPILER=="GCC"):
             LibName("OPENAL", "-framework AudioUnit")
             LibName("OPENAL", "-framework AudioToolbox")
             LibName("OPENAL", "-framework CoreAudio")
-        else:
+        elif GetTarget() != "emscripten":
             LibName("OPENAL", "-Wl,--exclude-libs,libopenal.a")
 
     if not PkgSkip("ASSIMP") and \
@@ -1010,7 +1031,7 @@ if (COMPILER=="GCC"):
         if os.path.isfile(irrxml):
             LibName("ASSIMP", irrxml)
 
-            if GetTarget() != "darwin":
+            if GetTarget() not in ("darwin", "emscripten"):
                 LibName("ASSIMP", "-Wl,--exclude-libs,libassimp.a")
                 LibName("ASSIMP", "-Wl,--exclude-libs,libIrrXML.a")
 
@@ -1018,19 +1039,31 @@ if (COMPILER=="GCC"):
         python_lib = SDK["PYTHONVERSION"]
         SmartPkgEnable("PYTHON", "", python_lib, (SDK["PYTHONVERSION"], SDK["PYTHONVERSION"] + "/Python.h"))
 
+        if not PkgSkip("PYTHON") and GetTarget() == "emscripten":
+            # Python may have been compiled with these requirements.
+            # Is there a cleaner way to check this?
+            LinkFlag("PYTHON", "-s USE_BZIP2=1 -s USE_SQLITE3=1")
+            if not PkgHasCustomLocation("PYTHON"):
+                python_libdir = GetThirdpartyDir() + "python/lib"
+                if os.path.isfile(python_libdir + "/libmpdec.a"):
+                    LibName("PYTHON", python_libdir + "/libmpdec.a")
+                if os.path.isfile(python_libdir + "/libexpat.a"):
+                    LibName("PYTHON", python_libdir + "/libexpat.a")
+
         if GetTarget() == "linux":
             LibName("PYTHON", "-lutil")
             LibName("PYTHON", "-lrt")
 
     SmartPkgEnable("OPENSSL",   "openssl",   ("ssl", "crypto"), ("openssl/ssl.h", "openssl/crypto.h"))
-    SmartPkgEnable("ZLIB",      "zlib",      ("z"), "zlib.h")
     SmartPkgEnable("GTK3",      "gtk+-3.0")
+    if GetTarget() != 'emscripten':
+       SmartPkgEnable("ZLIB",      "zlib",      ("z"), "zlib.h")
 
-    if not PkgSkip("OPENSSL") and GetTarget() != "darwin":
+    if not PkgSkip("OPENSSL") and GetTarget() not in ("darwin", "emscripten"):
         LibName("OPENSSL", "-Wl,--exclude-libs,libssl.a")
         LibName("OPENSSL", "-Wl,--exclude-libs,libcrypto.a")
 
-    if GetTarget() != 'darwin':
+    if GetTarget() not in ('darwin', 'emscripten'):
         # CgGL is covered by the Cg framework, and we don't need X11 components on OSX
         if not PkgSkip("NVIDIACG"):
             SmartPkgEnable("CGGL", "", ("CgGL"), "Cg/cgGL.h", thirdparty_dir = "nvidiacg")
@@ -1134,7 +1167,7 @@ if (COMPILER=="GCC"):
                 LibName(pkg, "-dylib_file /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib:/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib")
 
 DefSymbol("WITHINPANDA", "WITHIN_PANDA", "1")
-if GetLinkAllStatic():
+if GetLinkAllStatic() or GetTarget() == 'emscripten':
     DefSymbol("ALWAYS", "LINK_ALL_STATIC")
 if GetTarget() == 'android':
     DefSymbol("ALWAYS", "ANDROID")
@@ -1368,6 +1401,8 @@ def CompileCxx(obj,src,opts):
             if (opt=="ALWAYS") or (opt in opts): cmd += ' -F' + BracketNameWithQuotes(dir)
         for (opt,var,val) in DEFSYMBOLS:
             if (opt=="ALWAYS") or (opt in opts): cmd += ' -D' + var + '=' + val
+        for (opt,flag) in COMPILEFLAGS:
+            if (opt=="ALWAYS") or (opt in opts): cmd += ' ' + flag
         for x in ipath: cmd += ' -I' + x
 
         if not GetLinkAllStatic() and 'NOHIDDEN' not in opts:
@@ -1391,7 +1426,7 @@ def CompileCxx(obj,src,opts):
                 if 'NOARCH:' + arch.upper() not in opts:
                     cmd += " -arch %s" % arch
 
-        elif 'clang' not in GetCXX().split('/')[-1]:
+        elif 'clang' not in GetCXX().split('/')[-1] and GetCXX() != 'em++':
             # Enable interprocedural optimizations in GCC.
             cmd += " -fno-semantic-interposition"
 
@@ -1437,6 +1472,12 @@ def CompileCxx(obj,src,opts):
             if arch != 'arm64' and arch.startswith('arm') and PkgSkip("NEON") == 0:
                 cmd += ' -mfpu=neon'
 
+        elif GetTarget() == 'emscripten':
+            if GetOptimize() <= 1:
+                cmd += " -s ASSERTIONS=2"
+            elif GetOptimize() <= 2:
+                cmd += " -s ASSERTIONS=1"
+
         else:
             cmd += " -pthread"
 
@@ -1446,6 +1487,8 @@ def CompileCxx(obj,src,opts):
                 cmd += " -fexceptions"
             else:
                 cmd += " -fno-exceptions"
+                if GetTarget() == 'emscripten':
+                    cmd += " -s DISABLE_EXCEPTION_CATCHING=1"
 
                 if src.endswith(".mm"):
                     # Work around Apple compiler bug.
@@ -1458,7 +1501,8 @@ def CompileCxx(obj,src,opts):
                     cmd += " -fno-rtti"
 
         if ('SSE2' in opts or not PkgSkip("SSE2")) and not arch.startswith("arm") and arch != 'aarch64':
-            cmd += " -msse2"
+            if GetTarget() != "emscripten":
+                cmd += " -msse2"
 
         # Needed by both Python, Panda, Eigen, all of which break aliasing rules.
         cmd += " -fno-strict-aliasing"
@@ -1473,9 +1517,14 @@ def CompileCxx(obj,src,opts):
                 cmd += " -fno-finite-math-only"
 
             # Make sure this is off to avoid GCC/Eigen bug (see GitHub #228)
-            cmd += " -fno-unsafe-math-optimizations"
+            if GetTarget() != "emscripten":
+                cmd += " -fno-unsafe-math-optimizations"
 
-        if (optlevel==1): cmd += " -ggdb -D_DEBUG"
+        if (optlevel==1):
+            if GetTarget() == "emscripten":
+                cmd += " -g -D_DEBUG"
+            else:
+                cmd += " -ggdb -D_DEBUG"
         if (optlevel==2): cmd += " -O1 -D_DEBUG"
         if (optlevel==3): cmd += " -O2"
         if (optlevel==4): cmd += " -O3 -DNDEBUG"
@@ -1625,6 +1674,9 @@ def CompileIgate(woutd,wsrc,opts):
         elif target == 'android':
             cmd += ' -D__ANDROID__'
 
+    if GetTarget() == "emscripten":
+        cmd += ' -D__EMSCRIPTEN__'
+
     optlevel = GetOptimizeOption(opts)
     if (optlevel==1): cmd += ' -D_DEBUG'
     if (optlevel==2): cmd += ' -D_DEBUG'
@@ -1663,10 +1715,8 @@ def CompileIgate(woutd,wsrc,opts):
 def CompileImod(wobj, wsrc, opts):
     module = GetValueOption(opts, "IMOD:")
     library = GetValueOption(opts, "ILIB:")
-    if (COMPILER=="MSVC"):
-        woutc = wobj[:-4]+".cxx"
-    if (COMPILER=="GCC"):
-        woutc = wobj[:-2]+".cxx"
+    woutc = os.path.splitext(wobj)[0] + ".cxx"
+
     if (PkgSkip("PYTHON")):
         WriteFile(woutc, "")
         CompileCxx(wobj, woutc, opts)
@@ -1889,13 +1939,18 @@ def CompileLink(dll, obj, opts):
             else:
                 cmd = cxx + ' -shared'
                 # Always set soname on Android to avoid a linker warning when loading the library.
-                if "MODULE" not in opts or GetTarget() == 'android':
+                if GetTarget() == 'android' or ("MODULE" not in opts and GetTarget() != 'emscripten'):
                     cmd += " -Wl,-soname=" + os.path.basename(dll)
                 cmd += ' -o ' + dll + ' -L' + GetOutputDir() + '/lib -L' + GetOutputDir() + '/tmp'
 
-        for x in obj:
-            if GetOrigExt(x) != ".dat":
-                cmd += ' ' + x
+        if GetTarget() == 'emscripten' and GetOrigExt(dll) != ".exe":
+            for x in obj:
+                if GetOrigExt(x) not in (".dat", ".dll"):
+                    cmd += ' ' + x
+        else:
+            for x in obj:
+                if GetOrigExt(x) != ".dat":
+                    cmd += ' ' + x
 
         if (GetOrigExt(dll) == ".exe" and GetTarget() == 'windows' and "NOICON" not in opts):
             cmd += " " + GetOutputDir() + "/tmp/pandaIcon.res"
@@ -1929,6 +1984,13 @@ def CompileLink(dll, obj, opts):
             elif arch == 'mips':
                 cmd += ' -mips32'
             cmd += ' -lc -lm'
+
+        elif GetTarget() == 'emscripten':
+            cmd += " -s WARN_ON_UNDEFINED_SYMBOLS=1"
+            if GetOrigExt(dll) == ".exe":
+                cmd += " --memory-init-file 0"
+                cmd += " -s EXIT_RUNTIME=1"
+
         else:
             cmd += " -pthread"
             if "SYSROOT" in SDK:
@@ -1948,13 +2010,23 @@ def CompileLink(dll, obj, opts):
         for (opt, dir) in FRAMEWORKDIRECTORIES:
             if (opt=="ALWAYS") or (opt in opts):
                 cmd += ' -F' + BracketNameWithQuotes(dir)
-        for (opt, name) in LIBNAMES:
+        if GetOrigExt(dll) == ".exe" or GetTarget() != 'emscripten':
+            for (opt, name) in LIBNAMES:
+                if (opt=="ALWAYS") or (opt in opts):
+                    cmd += ' ' + BracketNameWithQuotes(name)
+        for (opt, flag) in LINKFLAGS:
             if (opt=="ALWAYS") or (opt in opts):
-                cmd += ' ' + BracketNameWithQuotes(name)
+                cmd += ' ' + flag
 
-        if GetTarget() != 'freebsd':
+        if GetTarget() not in ('freebsd', 'emscripten'):
             cmd += " -ldl"
 
+        if GetTarget() == 'emscripten':
+            optlevel = GetOptimizeOption(opts)
+            if optlevel == 2: cmd += " -O1"
+            if optlevel == 3: cmd += " -O2"
+            if optlevel == 4: cmd += " -O3"
+
         oscmd(cmd)
 
         if GetOptimizeOption(opts) == 4 and GetTarget() in ('linux', 'android'):
@@ -2278,7 +2350,7 @@ def CompileAnything(target, inputs, opts, progress = None):
                 if target.lower().endswith(".dylib"):
                     target = target[:-5] + MAJOR_VERSION + ".dylib"
                     SetOrigExt(target, origsuffix)
-            elif tplatform != "windows" and tplatform != "android":
+            elif tplatform not in ("windows", "android", "emscripten"):
                 # On Linux, libraries are named like libpanda.so.1.2
                 target += "." + MAJOR_VERSION
                 SetOrigExt(target, origsuffix)
@@ -2463,11 +2535,11 @@ PRC_PARAMETERS=[
     ("DEFAULT_PRC_DIR",                '"<auto>etc"',            '"<auto>etc"'),
     ("PRC_DIR_ENVVARS",                '"PANDA_PRC_DIR"',        '"PANDA_PRC_DIR"'),
     ("PRC_PATH_ENVVARS",               '"PANDA_PRC_PATH"',       '"PANDA_PRC_PATH"'),
-    ("PRC_PATH2_ENVVARS",              '""',                     '""'),
+    ("PRC_PATH2_ENVVARS",              'UNDEF',                  'UNDEF'),
     ("PRC_PATTERNS",                   '"*.prc"',                '"*.prc"'),
     ("PRC_ENCRYPTED_PATTERNS",         '"*.prc.pe"',             '"*.prc.pe"'),
     ("PRC_ENCRYPTION_KEY",             '""',                     '""'),
-    ("PRC_EXECUTABLE_PATTERNS",        '""',                     '""'),
+    ("PRC_EXECUTABLE_PATTERNS",        'UNDEF',                  'UNDEF'),
     ("PRC_EXECUTABLE_ARGS_ENVVAR",     '"PANDA_PRC_XARGS"',      '"PANDA_PRC_XARGS"'),
     ("PRC_PUBLIC_KEYS_FILENAME",       '""',                     '""'),
     ("PRC_RESPECT_TRUST_LEVEL",        'UNDEF',                  'UNDEF'),
@@ -2553,6 +2625,26 @@ def WriteConfigSettings():
         dtool_config["PHAVE_LOCKF"] = 'UNDEF'
         dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF'
 
+    if (GetTarget() == "emscripten"):
+        # There are no threads in JavaScript, so don't bother using them.
+        dtool_config["HAVE_THREADS"] = 'UNDEF'
+        dtool_config["DO_PIPELINING"] = 'UNDEF'
+        dtool_config["HAVE_POSIX_THREADS"] = 'UNDEF'
+        dtool_config["IS_LINUX"] = 'UNDEF'
+        dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF'
+        dtool_config["HAVE_NET"] = 'UNDEF'
+        dtool_config["PHAVE_LINUX_INPUT_H"] = 'UNDEF'
+        dtool_config["HAVE_X11"] = 'UNDEF'
+        dtool_config["HAVE_GLX"] = 'UNDEF'
+
+        # There are no environment vars either, or default prc files.
+        prc_parameters["DEFAULT_PRC_DIR"] = 'UNDEF'
+        prc_parameters["PRC_DIR_ENVVARS"] = 'UNDEF'
+        prc_parameters["PRC_PATH_ENVVARS"] = 'UNDEF'
+        prc_parameters["PRC_PATH2_ENVVARS"] = 'UNDEF'
+        prc_parameters["PRC_PATTERNS"] = 'UNDEF'
+        prc_parameters["PRC_ENCRYPTED_PATTERNS"] = 'UNDEF'
+
     if (GetOptimize() <= 2 and GetTarget() == "windows"):
         dtool_config["USE_DEBUG_PYTHON"] = '1'
 
@@ -2566,7 +2658,7 @@ def WriteConfigSettings():
     if (GetOptimize() <= 3):
         dtool_config["DO_COLLISION_RECORDING"] = '1'
 
-    if (GetOptimize() <= 3):
+    if (GetOptimize() <= 3) and GetTarget() != 'emscripten':
         dtool_config["DO_MEMORY_USAGE"] = '1'
 
     if (GetOptimize() <= 3):
@@ -3791,7 +3883,9 @@ TargetAdd('libp3gsgbase.in', opts=['IMOD:panda3d.core', 'ILIB:libp3gsgbase', 'SR
 OPTS=['DIR:panda/src/pnmimage', 'BUILDING:PANDA', 'ZLIB']
 TargetAdd('p3pnmimage_composite1.obj', opts=OPTS, input='p3pnmimage_composite1.cxx')
 TargetAdd('p3pnmimage_composite2.obj', opts=OPTS, input='p3pnmimage_composite2.cxx')
-TargetAdd('p3pnmimage_convert_srgb_sse2.obj', opts=OPTS+['SSE2'], input='convert_srgb_sse2.cxx')
+
+if GetTarget() != "emscripten":
+  TargetAdd('p3pnmimage_convert_srgb_sse2.obj', opts=OPTS+['SSE2'], input='convert_srgb_sse2.cxx')
 
 OPTS=['DIR:panda/src/pnmimage', 'ZLIB']
 IGATEFILES=GetDirectoryContents('panda/src/pnmimage', ["*.h", "*_composite*.cxx"])
@@ -3803,8 +3897,9 @@ PyTargetAdd('p3pnmimage_pfmFile_ext.obj', opts=OPTS, input='pfmFile_ext.cxx')
 # DIRECTORY: panda/src/nativenet/
 #
 
-OPTS=['DIR:panda/src/nativenet', 'BUILDING:PANDA']
-TargetAdd('p3nativenet_composite1.obj', opts=OPTS, input='p3nativenet_composite1.cxx')
+if GetTarget() != 'emscripten':
+  OPTS=['DIR:panda/src/nativenet', 'BUILDING:PANDA']
+  TargetAdd('p3nativenet_composite1.obj', opts=OPTS, input='p3nativenet_composite1.cxx')
 
 OPTS=['DIR:panda/src/nativenet']
 IGATEFILES=GetDirectoryContents('panda/src/nativenet', ["*.h", "*_composite*.cxx"])
@@ -3815,9 +3910,10 @@ TargetAdd('libp3nativenet.in', opts=['IMOD:panda3d.core', 'ILIB:libp3nativenet',
 # DIRECTORY: panda/src/net/
 #
 
-OPTS=['DIR:panda/src/net', 'BUILDING:PANDA']
-TargetAdd('p3net_composite1.obj', opts=OPTS, input='p3net_composite1.cxx')
-TargetAdd('p3net_composite2.obj', opts=OPTS, input='p3net_composite2.cxx')
+if GetTarget() != 'emscripten':
+  OPTS=['DIR:panda/src/net', 'BUILDING:PANDA']
+  TargetAdd('p3net_composite1.obj', opts=OPTS, input='p3net_composite1.cxx')
+  TargetAdd('p3net_composite2.obj', opts=OPTS, input='p3net_composite2.cxx')
 
 OPTS=['DIR:panda/src/net']
 IGATEFILES=GetDirectoryContents('panda/src/net', ["*.h", "*_composite*.cxx"])
@@ -4155,7 +4251,6 @@ TargetAdd('libpanda.dll', input='p3pnmimagetypes_composite1.obj')
 TargetAdd('libpanda.dll', input='p3pnmimagetypes_composite2.obj')
 TargetAdd('libpanda.dll', input='p3pnmimage_composite1.obj')
 TargetAdd('libpanda.dll', input='p3pnmimage_composite2.obj')
-TargetAdd('libpanda.dll', input='p3pnmimage_convert_srgb_sse2.obj')
 TargetAdd('libpanda.dll', input='p3text_composite1.obj')
 TargetAdd('libpanda.dll', input='p3text_composite2.obj')
 TargetAdd('libpanda.dll', input='p3tform_composite1.obj')
@@ -4165,14 +4260,17 @@ TargetAdd('libpanda.dll', input='p3putil_composite2.obj')
 TargetAdd('libpanda.dll', input='p3audio_composite1.obj')
 TargetAdd('libpanda.dll', input='p3pgui_composite1.obj')
 TargetAdd('libpanda.dll', input='p3pgui_composite2.obj')
-TargetAdd('libpanda.dll', input='p3net_composite1.obj')
-TargetAdd('libpanda.dll', input='p3net_composite2.obj')
-TargetAdd('libpanda.dll', input='p3nativenet_composite1.obj')
 TargetAdd('libpanda.dll', input='p3pandabase_pandabase.obj')
 TargetAdd('libpanda.dll', input='libpandaexpress.dll')
 TargetAdd('libpanda.dll', input='libp3dtoolconfig.dll')
 TargetAdd('libpanda.dll', input='libp3dtool.dll')
 
+if GetTarget() != "emscripten":
+  TargetAdd('libpanda.dll', input='p3net_composite1.obj')
+  TargetAdd('libpanda.dll', input='p3net_composite2.obj')
+  TargetAdd('libpanda.dll', input='p3nativenet_composite1.obj')
+  TargetAdd('libpanda.dll', input='p3pnmimage_convert_srgb_sse2.obj')
+
 if PkgSkip("FREETYPE")==0:
     TargetAdd('libpanda.dll', input="p3pnmtext_composite1.obj")
 
@@ -4210,11 +4308,13 @@ PyTargetAdd('core_module.obj', input='libp3text.in')
 PyTargetAdd('core_module.obj', input='libp3tform.in')
 PyTargetAdd('core_module.obj', input='libp3putil.in')
 PyTargetAdd('core_module.obj', input='libp3audio.in')
-PyTargetAdd('core_module.obj', input='libp3nativenet.in')
-PyTargetAdd('core_module.obj', input='libp3net.in')
 PyTargetAdd('core_module.obj', input='libp3pgui.in')
 PyTargetAdd('core_module.obj', input='libp3movies.in')
 
+if GetTarget() != "emscripten":
+  PyTargetAdd('core_module.obj', input='libp3nativenet.in')
+  PyTargetAdd('core_module.obj', input='libp3net.in')
+
 if PkgSkip("FREETYPE")==0:
     PyTargetAdd('core_module.obj', input='libp3pnmtext.in')
 
@@ -4256,8 +4356,10 @@ PyTargetAdd('core.pyd', input='libp3tform_igate.obj')
 PyTargetAdd('core.pyd', input='libp3putil_igate.obj')
 PyTargetAdd('core.pyd', input='libp3audio_igate.obj')
 PyTargetAdd('core.pyd', input='libp3pgui_igate.obj')
-PyTargetAdd('core.pyd', input='libp3net_igate.obj')
-PyTargetAdd('core.pyd', input='libp3nativenet_igate.obj')
+
+if GetTarget() != "emscripten":
+  PyTargetAdd('core.pyd', input='libp3net_igate.obj')
+  PyTargetAdd('core.pyd', input='libp3nativenet_igate.obj')
 
 if PkgSkip("FREETYPE")==0:
     PyTargetAdd('core.pyd', input="libp3pnmtext_igate.obj")
@@ -4635,7 +4737,7 @@ if not PkgSkip("EGG"):
 # DIRECTORY: panda/src/x11display/
 #
 
-if GetTarget() not in ['windows', 'darwin'] and not PkgSkip("X11"):
+if GetTarget() not in ['windows', 'darwin', 'emscripten'] and not PkgSkip("X11"):
     OPTS=['DIR:panda/src/x11display', 'BUILDING:PANDAX11', 'X11']
     TargetAdd('p3x11display_composite1.obj', opts=OPTS, input='p3x11display_composite1.cxx')
 
@@ -4643,7 +4745,7 @@ if GetTarget() not in ['windows', 'darwin'] and not PkgSkip("X11"):
 # DIRECTORY: panda/src/glxdisplay/
 #
 
-if GetTarget() not in ['windows', 'darwin'] and not PkgSkip("GL") and not PkgSkip("X11"):
+if GetTarget() not in ['windows', 'darwin', 'emscripten'] and not PkgSkip("GL") and not PkgSkip("X11"):
     DefSymbol('GLX', 'HAVE_GLX', '')
     OPTS=['DIR:panda/src/glxdisplay', 'BUILDING:PANDAGL', 'GL', 'NVIDIACG', 'CGGL', 'GLX']
     TargetAdd('p3glxdisplay_composite1.obj', opts=OPTS, input='p3glxdisplay_composite1.cxx')
@@ -4773,6 +4875,21 @@ if GetTarget() != 'android' and not PkgSkip("EGL") and not PkgSkip("GLES2"):
     TargetAdd('libpandagles2.dll', input=COMMON_PANDA_LIBS)
     TargetAdd('libpandagles2.dll', opts=['MODULE', 'GLES2', 'EGL', 'X11'])
 
+#
+# DIRECTORY: panda/src/webgldisplay/
+#
+
+if GetTarget() == 'emscripten' and not PkgSkip("GLES2"):
+  DefSymbol('GLES2', 'OPENGLES_2', '')
+  LinkFlag('GLES2', '-s GL_ENABLE_GET_PROC_ADDRESS=1')
+  OPTS=['DIR:panda/src/webgldisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES2',  'GLES2', 'WEBGL']
+  TargetAdd('p3webgldisplay_webgldisplay_composite1.obj', opts=OPTS, input='p3webgldisplay_composite1.cxx')
+  TargetAdd('libp3webgldisplay.dll', input='p3gles2gsg_config_gles2gsg.obj')
+  TargetAdd('libp3webgldisplay.dll', input='p3gles2gsg_gles2gsg.obj')
+  TargetAdd('libp3webgldisplay.dll', input='p3webgldisplay_webgldisplay_composite1.obj')
+  TargetAdd('libp3webgldisplay.dll', input=COMMON_PANDA_LIBS)
+  TargetAdd('libp3webgldisplay.dll', opts=['MODULE', 'GLES2', 'WEBGL'])
+
 #
 # DIRECTORY: panda/src/ode/
 #
@@ -4958,6 +5075,8 @@ if not PkgSkip("PVIEW"):
 
     if GetLinkAllStatic() and not PkgSkip("GL"):
         TargetAdd('pview.exe', input='libpandagl.dll')
+    if GetTarget() == "emscripten" and not PkgSkip("GLES2"):
+        TargetAdd('pview.exe', input='libp3webgldisplay.dll')
 
 #
 # DIRECTORY: panda/src/android/
@@ -5124,7 +5243,7 @@ if not PkgSkip("DIRECT"):
 # DIRECTORY: direct/src/distributed/
 #
 
-if not PkgSkip("DIRECT"):
+if not PkgSkip("DIRECT") and GetTarget() != 'emscripten':
     OPTS=['DIR:direct/src/distributed', 'DIR:direct/src/dcparser', 'WITHINPANDA', 'BUILDING:DIRECT']
     TargetAdd('p3distributed_config_distributed.obj', opts=OPTS, input='config_distributed.cxx')
 
@@ -5192,7 +5311,8 @@ if not PkgSkip("DIRECT"):
     if GetTarget() == 'darwin':
         TargetAdd('libp3direct.dll', input='p3showbase_showBase_assist.obj')
     TargetAdd('libp3direct.dll', input='p3deadrec_composite1.obj')
-    TargetAdd('libp3direct.dll', input='p3distributed_config_distributed.obj')
+    if GetTarget() != 'emscripten':
+        TargetAdd('libp3direct.dll', input='p3distributed_config_distributed.obj')
     TargetAdd('libp3direct.dll', input='p3interval_composite1.obj')
     TargetAdd('libp3direct.dll', input='p3motiontrail_config_motiontrail.obj')
     TargetAdd('libp3direct.dll', input='p3motiontrail_cMotionTrail.obj')
@@ -5203,7 +5323,8 @@ if not PkgSkip("DIRECT"):
     PyTargetAdd('direct_module.obj', input='libp3showbase.in')
     PyTargetAdd('direct_module.obj', input='libp3deadrec.in')
     PyTargetAdd('direct_module.obj', input='libp3interval.in')
-    PyTargetAdd('direct_module.obj', input='libp3distributed.in')
+    if GetTarget() != 'emscripten':
+        PyTargetAdd('direct_module.obj', input='libp3distributed.in')
     PyTargetAdd('direct_module.obj', input='libp3motiontrail.in')
     PyTargetAdd('direct_module.obj', opts=['IMOD:panda3d.direct', 'ILIB:direct', 'IMPORT:panda3d.core'])
 
@@ -5212,15 +5333,17 @@ if not PkgSkip("DIRECT"):
     PyTargetAdd('direct.pyd', input='libp3deadrec_igate.obj')
     PyTargetAdd('direct.pyd', input='libp3interval_igate.obj')
     PyTargetAdd('direct.pyd', input='p3interval_cInterval_ext.obj')
-    PyTargetAdd('direct.pyd', input='libp3distributed_igate.obj')
+    if GetTarget() != 'emscripten':
+        PyTargetAdd('direct.pyd', input='libp3distributed_igate.obj')
     PyTargetAdd('direct.pyd', input='libp3motiontrail_igate.obj')
 
     # These are part of direct.pyd, not libp3direct.dll, because they rely on
     # the Python libraries.  If a C++ user needs these modules, we can move them
     # back and filter out the Python-specific code.
     PyTargetAdd('direct.pyd', input='p3dcparser_ext_composite.obj')
-    PyTargetAdd('direct.pyd', input='p3distributed_cConnectionRepository.obj')
-    PyTargetAdd('direct.pyd', input='p3distributed_cDistributedSmoothNodeBase.obj')
+    if GetTarget() != 'emscripten':
+        PyTargetAdd('direct.pyd', input='p3distributed_cConnectionRepository.obj')
+        PyTargetAdd('direct.pyd', input='p3distributed_cDistributedSmoothNodeBase.obj')
 
     PyTargetAdd('direct.pyd', input='direct_module.obj')
     PyTargetAdd('direct.pyd', input='libp3direct.dll')
@@ -5886,7 +6009,7 @@ if not PkgSkip("PANDATOOL"):
 # DIRECTORY: pandatool/src/text-stats/
 #
 
-if not PkgSkip("PANDATOOL"):
+if not PkgSkip("PANDATOOL") and GetTarget() != 'emscripten':
     OPTS=['DIR:pandatool/src/text-stats']
     TargetAdd('text-stats_textMonitor.obj', opts=OPTS, input='textMonitor.cxx')
     TargetAdd('text-stats_textStats.obj', opts=OPTS, input='textStats.cxx')
@@ -6159,6 +6282,9 @@ if PkgSkip("PYTHON") == 0:
         PyTargetAdd('deploy-stub.exe', input='frozen_dllmain.obj')
     PyTargetAdd('deploy-stub.exe', opts=['WINSHELL', 'DEPLOYSTUB', 'NOICON', 'ANDROID'])
 
+    if GetTarget() == 'emscripten':
+        PyTargetAdd('deploy-stub.exe', opts=['ZLIB'])
+
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stubw.exe', input='deploy-stub.obj')
         PyTargetAdd('deploy-stubw.exe', input='frozen_dllmain.obj')

+ 68 - 16
makepanda/makepandacore.py

@@ -46,6 +46,14 @@ ANDROID_API = None
 SYS_LIB_DIRS = []
 SYS_INC_DIRS = []
 DEBUG_DEPENDENCIES = False
+if sys.platform == "darwin" or sys.platform.startswith("freebsd"):
+    DEFAULT_CC = "clang"
+    DEFAULT_CXX = "clang++"
+else:
+    DEFAULT_CC = "gcc"
+    DEFAULT_CXX = "g++"
+DEFAULT_AR = "ar"
+DEFAULT_RANLIB = "ranlib"
 
 # Is the current Python a 32-bit or 64-bit build?  There doesn't
 # appear to be a universal test for this.
@@ -353,6 +361,7 @@ def SetTarget(target, arch=None):
     be called *before* any calls are made to GetOutputDir, GetCC, etc."""
     global TARGET, TARGET_ARCH, HAS_TARGET_ARCH
     global TOOLCHAIN_PREFIX
+    global DEFAULT_CC, DEFAULT_CXX, DEFAULT_AR, DEFAULT_RANLIB
 
     host = GetHost()
     host_arch = GetHostArch()
@@ -376,6 +385,9 @@ def SetTarget(target, arch=None):
             exit("Windows architecture must be x86 or x64")
 
     elif target == 'darwin':
+        DEFAULT_CC = "clang"
+        DEFAULT_CXX = "clang++"
+
         if arch == 'amd64':
             arch = 'x86_64'
         if arch == 'aarch64':
@@ -437,6 +449,8 @@ def SetTarget(target, arch=None):
 
         ANDROID_TRIPLE += str(ANDROID_API)
         TOOLCHAIN_PREFIX = ANDROID_TRIPLE + '-'
+        DEFAULT_CC = "clang"
+        DEFAULT_CXX = "clang++"
 
     elif target == 'linux':
         if arch is not None:
@@ -445,7 +459,19 @@ def SetTarget(target, arch=None):
         elif host != 'linux':
             exit('Should specify an architecture when building for Linux')
 
+    elif target == 'emscripten':
+        DEFAULT_CC = "emcc"
+        DEFAULT_CXX = "em++"
+        DEFAULT_AR = "emar"
+        DEFAULT_RANLIB = "emranlib"
+
+        arch = "wasm32"
+
     elif target == host:
+        if target == 'freebsd':
+            DEFAULT_CC = "clang"
+            DEFAULT_CXX = "clang++"
+
         if arch is None or arch == host_arch:
             # Not a cross build.
             pass
@@ -486,16 +512,10 @@ def CrossCompiling():
     return GetTarget() != GetHost()
 
 def GetCC():
-    if TARGET in ('darwin', 'freebsd', 'android'):
-        return os.environ.get('CC', TOOLCHAIN_PREFIX + 'clang')
-    else:
-        return os.environ.get('CC', TOOLCHAIN_PREFIX + 'gcc')
+    return os.environ.get('CC', TOOLCHAIN_PREFIX + DEFAULT_CC)
 
 def GetCXX():
-    if TARGET in ('darwin', 'freebsd', 'android'):
-        return os.environ.get('CXX', TOOLCHAIN_PREFIX + 'clang++')
-    else:
-        return os.environ.get('CXX', TOOLCHAIN_PREFIX + 'g++')
+    return os.environ.get('CXX', TOOLCHAIN_PREFIX + DEFAULT_CXX)
 
 def GetStrip():
     # Hack
@@ -507,16 +527,16 @@ def GetStrip():
 def GetAR():
     # Hack
     if TARGET == 'android':
-        return TOOLCHAIN_PREFIX + 'ar'
+        return TOOLCHAIN_PREFIX + DEFAULT_AR
     else:
-        return 'ar'
+        return DEFAULT_AR
 
 def GetRanlib():
     # Hack
     if TARGET == 'android':
-        return TOOLCHAIN_PREFIX + 'ranlib'
+        return TOOLCHAIN_PREFIX + DEFAULT_RANLIB
     else:
-        return 'ranlib'
+        return DEFAULT_RANLIB
 
 BISON = None
 def GetBison():
@@ -1393,6 +1413,9 @@ def GetThirdpartyDir():
     elif (target == 'android'):
         THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch)
 
+    elif (target == 'emscripten'):
+        THIRDPARTYDIR = base + "/emscripten-libs/"
+
     else:
         Warn("Unsupported platform:", target)
         return
@@ -2837,6 +2860,8 @@ LIBDIRECTORIES = []
 FRAMEWORKDIRECTORIES = []
 LIBNAMES = []
 DEFSYMBOLS = []
+COMPILEFLAGS = []
+LINKFLAGS = []
 
 def IncDirectory(opt, dir):
     INCDIRECTORIES.append((opt, dir))
@@ -2900,6 +2925,12 @@ def LibName(opt, name):
 def DefSymbol(opt, sym, val=""):
     DEFSYMBOLS.append((opt, sym, val))
 
+def CompileFlag(opt, flag):
+    COMPILEFLAGS.append((opt, flag))
+
+def LinkFlag(opt, flag):
+    LINKFLAGS.append((opt, flag))
+
 ########################################################################
 #
 # This subroutine prepares the environment for the build.
@@ -2965,7 +2996,9 @@ def SetupBuildEnvironment(compiler):
             sysroot_flag = " -target " + ANDROID_TRIPLE
 
         # Extract the dirs from the line that starts with 'libraries: ='.
-        cmd = GetCXX() + " -print-search-dirs" + sysroot_flag
+        # The -E is mostly to keep emscripten happy by preventing it from
+        # running the compiler and complaining about the lack of input files.
+        cmd = GetCXX() + " -E -print-search-dirs" + sysroot_flag
         handle = os.popen(cmd)
         for line in handle:
             if not line.startswith('libraries: ='):
@@ -3373,7 +3406,8 @@ def SetOrigExt(x, v):
     ORIG_EXT[x] = v
 
 def GetExtensionSuffix():
-    if GetTarget() == 'windows':
+    target = GetTarget()
+    if target == 'windows':
         if GetOptimize() <= 2:
             dllext = '_d'
         else:
@@ -3383,6 +3417,8 @@ def GetExtensionSuffix():
             return dllext + '.cp%d%d-win_amd64.pyd' % (sys.version_info[:2])
         else:
             return dllext + '.cp%d%d-win32.pyd' % (sys.version_info[:2])
+    elif target == 'emscripten':
+        return '.so'
     elif CrossCompiling():
         return '.{0}.so'.format(GetPythonABI())
     else:
@@ -3453,6 +3489,15 @@ def CalcLocation(fn, ipath):
         if (fn.endswith(".rsrc")):  return OUTPUTDIR+"/tmp/"+fn
         if (fn.endswith(".plugin")):return OUTPUTDIR+"/plugins/"+fn
         if (fn.endswith(".app")):   return OUTPUTDIR+"/bin/"+fn
+    elif (target == 'emscripten'):
+        if (fn.endswith(".obj")):   return OUTPUTDIR+"/tmp/"+fn[:-4]+".o"
+        if (fn.endswith(".dll")):   return OUTPUTDIR+"/lib/"+fn[:-4]+".o"
+        if (fn.endswith(".pyd")):   return OUTPUTDIR+"/panda3d/"+fn[:-4]+".o"
+        if (fn.endswith(".mll")):   return OUTPUTDIR+"/plugins/"+fn
+        if (fn.endswith(".plugin")):return OUTPUTDIR+"/plugins/"+fn[:-7]+dllext+".js"
+        if (fn.endswith(".exe")):   return OUTPUTDIR+"/bin/"+fn[:-4]+".js"
+        if (fn.endswith(".lib")):   return OUTPUTDIR+"/lib/"+fn[:-4]+".a"
+        if (fn.endswith(".ilb")):   return OUTPUTDIR+"/tmp/"+fn[:-4]+".a"
     else:
         if (fn.endswith(".obj")):   return OUTPUTDIR+"/tmp/"+fn[:-4]+".o"
         if (fn.endswith(".dll")):   return OUTPUTDIR+"/lib/"+fn[:-4]+".so"
@@ -3677,10 +3722,17 @@ def TargetAdd(target, dummy=0, opts=[], input=[], dep=[], ipath=None, winrc=None
         if GetLinkAllStatic() and ORIG_EXT[fullinput] == '.lib' and fullinput in TARGET_TABLE:
             tdep = TARGET_TABLE[fullinput]
             for y in tdep.inputs:
-                if ORIG_EXT[y] == '.lib':
+                if ORIG_EXT[y] == '.lib' and y not in t.inputs:
                     t.inputs.append(y)
 
-            for opt, _ in LIBNAMES + LIBDIRECTORIES + FRAMEWORKDIRECTORIES:
+            for opt, _ in LIBNAMES + LIBDIRECTORIES + FRAMEWORKDIRECTORIES + LINKFLAGS + COMPILEFLAGS:
+                if opt in tdep.opts and opt not in t.opts:
+                    t.opts.append(opt)
+
+        elif GetTarget() == 'emscripten' and ORIG_EXT[fullinput] == '.dll' and fullinput in TARGET_TABLE:
+            # Transfer over flags like -s USE_LIBPNG=1
+            tdep = TARGET_TABLE[fullinput]
+            for opt, _ in LINKFLAGS:
                 if opt in tdep.opts and opt not in t.opts:
                     t.opts.append(opt)
 

+ 1 - 1
panda/src/downloader/httpAuthorization.cxx

@@ -16,7 +16,7 @@
 #include "urlSpec.h"
 #include "string_utils.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 using std::string;
 

+ 1 - 1
panda/src/downloader/httpAuthorization.h

@@ -20,7 +20,7 @@
 // use any OpenSSL code, because it is a support module for HTTPChannel, which
 // *does* use OpenSSL code.
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 #include "referenceCount.h"
 #include "httpEnum.h"

+ 1 - 1
panda/src/downloader/httpBasicAuthorization.cxx

@@ -13,7 +13,7 @@
 
 #include "httpBasicAuthorization.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 using std::string;
 

+ 1 - 1
panda/src/downloader/httpBasicAuthorization.h

@@ -20,7 +20,7 @@
 // use any OpenSSL code, because it is a support module for HTTPChannel, which
 // *does* use OpenSSL code.
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 #include "httpAuthorization.h"
 

+ 3 - 0
panda/src/downloader/httpChannel.h

@@ -453,6 +453,9 @@ std::ostream &operator << (std::ostream &out, HTTPChannel::State state);
 
 #include "httpChannel.I"
 
+#elif defined(__EMSCRIPTEN__)
+#include "httpChannel_emscripten.h"
+
 #endif  // HAVE_OPENSSL
 
 #endif

+ 442 - 0
panda/src/downloader/httpChannel_emscripten.I

@@ -0,0 +1,442 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpChannel_emscripten.I
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+/**
+ * Returns the HTTPClient object that owns this channel.
+ */
+INLINE HTTPClient *HTTPChannel::
+get_client() const {
+  return _client;
+}
+
+/**
+ * Returns true if the last-requested document was successfully retrieved and
+ * is ready to be read, false otherwise.
+ */
+INLINE bool HTTPChannel::
+is_valid() const {
+  return get_status_code() / 100 == 2;
+}
+
+/**
+ * Returns the URL that was used to retrieve the most recent document:
+ * whatever URL was last passed to get_document() or get_header().  If a
+ * redirect has transparently occurred, this will return the new, redirected
+ * URL (the actual URL at which the document was located).
+ */
+INLINE const URLSpec &HTTPChannel::
+get_url() const {
+  return _document_spec.get_url();
+}
+
+/**
+ * Returns the DocumentSpec associated with the most recent document.  This
+ * includes its actual URL (following redirects) along with the identity tag
+ * and last-modified date, if supplied by the server.
+ *
+ * This structure may be saved and used to retrieve the same version of the
+ * document later, or to conditionally retrieve a newer version if it is
+ * available.
+ */
+INLINE const DocumentSpec &HTTPChannel::
+get_document_spec() const {
+  return _document_spec;
+}
+
+/**
+ * Returns the HTML return code from the document retrieval request.  This
+ * will be in the 200 range if the document is successfully retrieved, or some
+ * other value in the case of an error.
+ *
+ * Some proxy errors during an https-over-proxy request would return the same
+ * status code as a different error that occurred on the host server.  To
+ * differentiate these cases, status codes that are returned by the proxy
+ * during the CONNECT phase (except code 407) are incremented by 1000.
+ */
+INLINE int HTTPChannel::
+get_status_code() const {
+  return _status_entry._status_code;
+}
+
+/**
+ * Returns the string as returned by the server describing the status code for
+ * humans.  This may or may not be meaningful.
+ */
+INLINE std::string HTTPChannel::
+get_status_string() const {
+  return _status_entry._status_string;
+}
+
+/**
+ * If the document failed to connect because of a 401 (Authorization
+ * required), this method will return the "realm" returned by the server in
+ * which the requested document must be authenticated.  This string may be
+ * presented to the user to request an associated username and password (which
+ * then should be stored in HTTPClient::set_username()).
+ */
+INLINE const std::string &HTTPChannel::
+get_www_realm() const {
+  return _www_realm;
+}
+
+/**
+ * Specifies the Content-Type header, useful for applications that require
+ * different types of content, such as JSON.
+ */
+INLINE void HTTPChannel::
+set_content_type(std::string content_type) {
+  _content_type = content_type;
+}
+
+/**
+ * Returns the value of the Content-Type header.
+ */
+INLINE std::string HTTPChannel::
+get_content_type() const {
+  return _content_type;
+}
+
+/**
+ * This may be called immediately after a call to get_document() or some
+ * related function to specify the expected size of the document we are
+ * retrieving, if we happen to know.  This is used as the return value to
+ * get_file_size() only in the case that the server does not tell us the
+ * actual file size.
+ */
+INLINE void HTTPChannel::
+set_expected_file_size(size_t file_size) {
+  _expected_file_size = file_size;
+  _got_expected_file_size = true;
+}
+
+/**
+ * Returns true if the size of the file we are currently retrieving was told
+ * us by the server and thus is reliably known, or false if the size reported
+ * by get_file_size() represents an educated guess (possibly as set by
+ * set_expected_file_size(), or as inferred from a chunked transfer encoding
+ * in progress).
+ */
+INLINE bool HTTPChannel::
+is_file_size_known() const {
+  return _got_file_size;
+}
+
+/**
+ * Returns the first byte of the file requested by the request.  This will
+ * normally be 0 to indicate that the file is being requested from the
+ * beginning, but if the file was requested via a get_subdocument() call, this
+ * will contain the first_byte parameter from that call.
+ */
+INLINE size_t HTTPChannel::
+get_first_byte_requested() const {
+  return _first_byte_requested;
+}
+
+/**
+ * Returns the last byte of the file requested by the request.  This will
+ * normally be 0 to indicate that the file is being requested to its last
+ * byte, but if the file was requested via a get_subdocument() call, this will
+ * contain the last_byte parameter from that call.
+ */
+INLINE size_t HTTPChannel::
+get_last_byte_requested() const {
+  return _last_byte_requested;
+}
+
+/**
+ * Returns the first byte of the file (that will be) delivered by the server
+ * in response to the current request.  Normally, this is the same as
+ * get_first_byte_requested(), but some servers will ignore a subdocument
+ * request and always return the whole file, in which case this value will be
+ * 0, regardless of what was requested to get_subdocument().
+ */
+INLINE size_t HTTPChannel::
+get_first_byte_delivered() const {
+  return _first_byte_delivered;
+}
+
+/**
+ * Returns the last byte of the file (that will be) delivered by the server in
+ * response to the current request.  Normally, this is the same as
+ * get_last_byte_requested(), but some servers will ignore a subdocument
+ * request and always return the whole file, in which case this value will be
+ * 0, regardless of what was requested to get_subdocument().
+ */
+INLINE size_t HTTPChannel::
+get_last_byte_delivered() const {
+  return _last_byte_delivered;
+}
+
+/**
+ * Stops whatever file transaction is currently in progress, closes the
+ * connection, and resets to begin anew.  You shouldn't ever need to call
+ * this, since the channel should be able to reset itself cleanly between
+ * requests, but it is provided in case you are an especially nervous type.
+ *
+ * Don't call this after every request unless you set
+ * set_persistent_connection() to false, since calling reset() rudely closes
+ * the connection regardless of whether we have told the server we intend to
+ * keep it open or not.
+ */
+INLINE void HTTPChannel::
+reset() {
+  reset_for_new_request();
+  _status_list.clear();
+}
+
+/**
+ * Preserves the previous status code (presumably a failure) from the previous
+ * connection attempt.  If the subsequent connection attempt also fails, the
+ * returned status code will be the better of the previous code and the
+ * current code.
+ *
+ * This can be called to daisy-chain subsequent attempts to download the same
+ * document from different servers.  After all servers have been attempted,
+ * the final status code will reflect the attempt that most nearly succeeded.
+ */
+INLINE void HTTPChannel::
+preserve_status() {
+  _status_list.push_back(_status_entry);
+}
+
+/**
+ * Resets the extra headers that were previously added via calls to
+ * send_extra_header().
+ */
+INLINE void HTTPChannel::
+clear_extra_headers() {
+  _send_extra_headers.clear();
+}
+
+/**
+ * Specifies an additional key: value pair that is added into the header sent
+ * to the server with the next request.  This is passed along with no
+ * interpretation by the HTTPChannel code.  You may call this repeatedly to
+ * append multiple headers.
+ *
+ * This is persistent for one request only; it must be set again for each new
+ * request.
+ */
+INLINE void HTTPChannel::
+send_extra_header(const std::string &key, const std::string &value) {
+  _send_extra_headers.push_back(std::make_pair(key, value));
+}
+
+/**
+ * Opens the named document for reading, if available.  Returns true if
+ * successful, false otherwise.
+ */
+INLINE bool HTTPChannel::
+get_document(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_get, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Retrieves only the specified byte range of the indicated document.  If
+ * last_byte is 0, it stands for the last byte of the document.  When a
+ * subdocument is requested, get_file_size() and get_bytes_downloaded() will
+ * report the number of bytes of the subdocument, not of the complete
+ * document.
+ */
+INLINE bool HTTPChannel::
+get_subdocument(const DocumentSpec &url, size_t first_byte, size_t last_byte) {
+  if (!begin_request(HTTPEnum::M_get, url, std::string(), false, first_byte, last_byte)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Like get_document(), except only the header associated with the document is
+ * retrieved.  This may be used to test for existence of the document; it
+ * might also return the size of the document (if the server gives us this
+ * information).
+ */
+INLINE bool HTTPChannel::
+get_header(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_head, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Posts form data to a particular URL and retrieves the response.
+ */
+INLINE bool HTTPChannel::
+post_form(const DocumentSpec &url, const std::string &body) {
+  if (!begin_request(HTTPEnum::M_post, url, body, false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Uploads the indicated body to the server to replace the indicated URL, if
+ * the server allows this.
+ */
+INLINE bool HTTPChannel::
+put_document(const DocumentSpec &url, const std::string &body) {
+  if (!begin_request(HTTPEnum::M_put, url, body, false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Requests the server to remove the indicated URL.
+ */
+INLINE bool HTTPChannel::
+delete_document(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_delete, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Sends an OPTIONS message to the server, which should query the available
+ * options, possibly in relation to a specified URL.
+ */
+INLINE bool HTTPChannel::
+get_options(const DocumentSpec &url) {
+  if (!begin_request(HTTPEnum::M_options, url, std::string(), false, 0, 0)) {
+    return false;
+  }
+  while (run()) {
+  }
+  return is_valid();
+}
+
+/**
+ * Begins a non-blocking request to retrieve a given document.  This method
+ * will return immediately, even before a connection to the server has
+ * necessarily been established; you must then call run() from time to time
+ * until the return value of run() is false.  Then you may check is_valid()
+ * and get_status_code() to determine the status of your request.
+ *
+ * If a previous request had been pending, that request is discarded.
+ */
+INLINE void HTTPChannel::
+begin_get_document(const DocumentSpec &url) {
+  begin_request(HTTPEnum::M_get, url, std::string(), true, 0, 0);
+}
+
+/**
+ * Begins a non-blocking request to retrieve only the specified byte range of
+ * the indicated document.  If last_byte is 0, it stands for the last byte of
+ * the document.  When a subdocument is requested, get_file_size() and
+ * get_bytes_downloaded() will report the number of bytes of the subdocument,
+ * not of the complete document.
+ */
+INLINE void HTTPChannel::
+begin_get_subdocument(const DocumentSpec &url, size_t first_byte,
+                      size_t last_byte) {
+  begin_request(HTTPEnum::M_get, url, std::string(), true, first_byte, last_byte);
+}
+
+/**
+ * Begins a non-blocking request to retrieve a given header.  See
+ * begin_get_document() and get_header().
+ */
+INLINE void HTTPChannel::
+begin_get_header(const DocumentSpec &url) {
+  begin_request(HTTPEnum::M_head, url, std::string(), true, 0, 0);
+}
+
+/**
+ * Posts form data to a particular URL and retrieves the response, all using
+ * non-blocking I/O.  See begin_get_document() and post_form().
+ *
+ * It is important to note that you *must* call run() repeatedly after calling
+ * this method until run() returns false, and you may not call any other
+ * document posting or retrieving methods using the HTTPChannel object in the
+ * interim, or your form data may not get posted.
+ */
+INLINE void HTTPChannel::
+begin_post_form(const DocumentSpec &url, const std::string &body) {
+  begin_request(HTTPEnum::M_post, url, body, true, 0, 0);
+}
+
+/**
+ * Returns the number of bytes downloaded during the last (or current)
+ * download_to_file() or download_to_ram operation().  This can be used in
+ * conjunction with get_file_size() to report the percent complete (but be
+ * careful, since get_file_size() may return 0 if the server has not told us
+ * the size of the file).
+ */
+INLINE size_t HTTPChannel::
+get_bytes_downloaded() const {
+  return _bytes_downloaded;
+}
+
+/**
+ * When download throttling is in effect (set_download_throttle() has been set
+ * to true) and non-blocking I/O methods (like begin_get_document()) are used,
+ * this returns the number of bytes "requested" from the server so far: that
+ * is, the theoretical maximum value for get_bytes_downloaded(), if the server
+ * has been keeping up with our demand.
+ *
+ * If this number is less than get_bytes_downloaded(), then the server has not
+ * been supplying bytes fast enough to meet our own download throttle rate.
+ *
+ * When download throttling is not in effect, or when the blocking I/O methods
+ * (like get_document(), etc.) are used, this returns 0.
+ */
+INLINE size_t HTTPChannel::
+get_bytes_requested() const {
+  return _bytes_requested;
+}
+
+/**
+ * Returns true when a download_to() or download_to_ram() has executed and the
+ * file has been fully downloaded.  If this still returns false after
+ * processing has completed, there was an error in transmission.
+ *
+ * Note that simply testing is_download_complete() does not prove that the
+ * requested document was successfully retrieved--you might have just
+ * downloaded the "404 not found" stub (for instance) that a server would
+ * provide in response to some error condition.  You should also check
+ * is_valid() to prove that the file you expected has been successfully
+ * retrieved.
+ */
+INLINE bool HTTPChannel::
+is_download_complete() const {
+  if (_download_dest != DD_none) {
+    return get_state() == S_done;
+  }
+  return false;
+}
+
+/**
+ *
+ */
+INLINE HTTPChannel::StatusEntry::
+StatusEntry() {
+  _status_code = SC_incomplete;
+}

+ 790 - 0
panda/src/downloader/httpChannel_emscripten.cxx

@@ -0,0 +1,790 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpChannel_emscripten.cxx
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#include "httpChannel_emscripten.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "string_utils.h"
+
+#include <emscripten/em_asm.h>
+
+#define _NOTIFY_HTTP_CHANNEL_ID   "[" << this << "] "
+
+#ifndef CPPPARSER
+extern "C" char *EMSCRIPTEN_KEEPALIVE
+_extend_string(std::string *str, int length) {
+  size_t offset = str->size();
+  str->resize(offset + (size_t)length);
+  return (char *)str->data() + offset;
+}
+
+extern "C" void EMSCRIPTEN_KEEPALIVE
+_write_stream(std::ostream *strm, const char *data, int length) {
+  strm->write(data, (size_t)length);
+}
+
+extern "C" bool EMSCRIPTEN_KEEPALIVE
+_http_channel_run(HTTPChannel *channel) {
+  return channel->run();
+}
+#endif
+
+TypeHandle HTTPChannel::_type_handle;
+
+/**
+ *
+ */
+HTTPChannel::
+HTTPChannel(HTTPClient *client) :
+  _client(client)
+{
+  if (downloader_cat.is_debug()) {
+    downloader_cat.debug()
+      << _NOTIFY_HTTP_CHANNEL_ID
+    << "created.\n";
+  }
+
+  EM_ASM({
+    if (!window._httpChannels) {
+      window._httpChannels = {};
+    }
+    var xhr = new XMLHttpRequest();
+    window._httpChannels[$0] = xhr;
+  }, this);
+
+  // _nonblocking is true if the XHR is actually in non-blocking mode.
+  _nonblocking = false;
+
+  // _wanted_nonblocking is true if the user specifically requested one of the
+  // non-blocking interfaces.  It is false if the XHR is only incidentally
+  // non-blocking (for instance, because ASYNCIFY is on).
+  _wanted_nonblocking = false;
+
+  _first_byte_requested = 0;
+  _last_byte_requested = 0;
+  _first_byte_delivered = 0;
+  _last_byte_delivered = 0;
+  _expected_file_size = 0;
+  _file_size = 0;
+  _transfer_file_size = 0;
+  _got_expected_file_size = false;
+  _got_file_size = false;
+  _got_transfer_file_size = false;
+  _bytes_downloaded = 0;
+  _bytes_requested = 0;
+  _status_entry = StatusEntry();
+  _content_type = "application/x-www-form-urlencoded";
+  _download_dest = DD_none;
+  _download_to_ramfile = nullptr;
+  _download_to_stream = nullptr;
+}
+
+/**
+ *
+ */
+HTTPChannel::
+~HTTPChannel() {
+  EM_ASM({
+    var xhr = window._httpChannels[$0];
+    if (xhr) {
+      xhr.onprogress = null;
+      xhr.onreadystatechange = null;
+      delete window._httpChannels[$0];
+    }
+  }, this);
+
+  if (downloader_cat.is_debug()) {
+    downloader_cat.debug()
+      << _NOTIFY_HTTP_CHANNEL_ID
+    << "destroyed.\n";
+  }
+
+  reset_download_to();
+}
+
+/**
+ * Returns the HTML header value associated with the indicated key, or empty
+ * string if the key was not defined in the message returned by the server.
+ */
+std::string HTTPChannel::
+get_header_value(const std::string &key) const {
+  Headers::const_iterator hi = _headers.find(downcase(key));
+  if (hi != _headers.end()) {
+    return (*hi).second;
+  }
+  return std::string();
+}
+
+/**
+ * Returns the size of the file, if it is known.  Returns the value set by
+ * set_expected_file_size() if the file size is not known, or 0 if this value
+ * was not set.
+ *
+ * If the file is dynamically generated, the size may not be available until a
+ * read has started (e.g.  open_read_body() has been called); and even then it
+ * may increase as more of the file is read due to the nature of HTTP/1.1
+ * requests which can change their minds midstream about how much data they're
+ * sending you.
+ */
+std::streamsize HTTPChannel::
+get_file_size() const {
+  if (_got_file_size) {
+    return _file_size;
+  } else if (_got_transfer_file_size) {
+    return _transfer_file_size;
+  } else if (_got_expected_file_size) {
+    return _expected_file_size;
+  } else {
+    return 0;
+  }
+}
+
+/**
+ * Outputs a list of all headers defined by the server to the indicated output
+ * stream.
+ */
+void HTTPChannel::
+write_headers(std::ostream &out) const {
+  Headers::const_iterator hi;
+  for (hi = _headers.begin(); hi != _headers.end(); ++hi) {
+    out << (*hi).first << ": " << (*hi).second << "\n";
+  }
+}
+
+/**
+ * This must be called from time to time when non-blocking I/O is in use.  It
+ * checks for data coming in on the socket and writes data out to the socket
+ * when possible, and does whatever processing is required towards completing
+ * the current task.
+ *
+ * The return value is true if the task is still pending (and run() will need
+ * to be called again in the future), or false if the current task is
+ * complete.
+ */
+bool HTTPChannel::
+run() {
+  if (downloader_cat.is_spam()) {
+    downloader_cat.spam()
+      << _NOTIFY_HTTP_CHANNEL_ID
+      << "run().\n";
+  }
+
+  State state = get_state();
+  switch (state) {
+  case S_unsent:
+    // Invalid.
+    return false;
+
+  case S_opened:
+    if (!run_send()) {
+      return false;
+    }
+    break;
+
+  case S_headers_received:
+    if (!run_headers_received()) {
+      return false;
+    }
+    break;
+
+  case S_loading:
+    if (_download_dest != DD_none) {
+      return false;
+    }
+    break;
+
+  case S_done:
+    close_download_stream();
+    return false;
+  }
+
+  // If we get here, we must be running in non-blocking mode.
+  if (!_wanted_nonblocking && emscripten_has_asyncify()) {
+    // But we are pretending to be in blocking mode, so we must yield until the
+    // state changes.
+    while (get_state() != state) {
+      emscripten_sleep(0);
+    }
+  }
+
+  return true;
+}
+
+/**
+ * Returns the current readyState of the XMLHttpRequest object.
+ */
+HTTPChannel::State HTTPChannel::
+get_state() const {
+  return (State)EM_ASM_INT(return window._httpChannels[$0].readyState, this);
+}
+
+/**
+ * Calls XHR.send().
+ */
+bool HTTPChannel::
+run_send() {
+  for (const ExtraHeader &header : _send_extra_headers) {
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.setRequestHeader(UTF8ToString($1), UTF8ToString($2));
+    }, this, header.first.c_str(), header.second.c_str());
+  }
+
+  if (_method == HTTPEnum::M_get || _method == HTTPEnum::M_head) {
+    // No body is sent with GET / HEAD requests.
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.send(null);
+    }, this);
+  }
+  else {
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.setRequestHeader("Content-Type", UTF8ToString($2));
+      xhr.send(UTF8ToString($1));
+    }, this, _body.c_str(), _content_type.c_str());
+  }
+
+  return true;
+}
+
+/**
+ * Called when the headers have been received.
+ */
+bool HTTPChannel::
+run_headers_received() {
+  char status_string[512];
+  char *header_str = nullptr;
+  status_string[0] = 0;
+
+  // Fetch the status code, text and response headers from JavaScript.
+  int status_code = EM_ASM_INT({
+    var xhr = window._httpChannels[$0];
+    stringToUTF8(xhr.statusText, $1, 512);
+
+    var headers = xhr.getAllResponseHeaders();
+    var len = lengthBytesUTF8(headers) + 1;
+    var buffer = _malloc(len);
+    stringToUTF8(headers, buffer, len);
+    setValue($2, buffer, '*');
+
+    return xhr.status;
+  }, this, status_string, &header_str);
+  _status_entry._status_code = status_code;
+
+  // Parse the response header string.
+  char *ptr = header_str;
+  char *delim = strstr(ptr, ": ");
+  while (delim != nullptr) {
+    std::string key(ptr, delim);
+    ptr = delim + 2;
+
+    std::string value;
+    delim = strstr(ptr, "\r\n");
+    if (delim != nullptr) {
+      value.assign(ptr, delim);
+      ptr = delim + 2;
+      delim = strstr(ptr, ": ");
+    }
+    else {
+      // The XHR spec prescribes that there is always another CRLF
+      // after the last header, but we handle this case anyway.
+      value.assign(ptr);
+    }
+
+    _headers[std::move(key)] = std::move(value);
+  }
+  free(header_str);
+
+  // Look for key properties in the header fields.
+  if (status_code == 206) {
+    std::string content_range = get_header_value("Content-Range");
+    if (content_range.empty()) {
+      downloader_cat.warning()
+        << _NOTIFY_HTTP_CHANNEL_ID
+        << "Got 206 response without Content-Range header!\n";
+      _status_entry._status_code = SC_invalid_http;
+      return false;
+
+    } else {
+      if (!parse_content_range(content_range)) {
+        downloader_cat.warning()
+          << _NOTIFY_HTTP_CHANNEL_ID
+          << "Couldn't parse Content-Range: " << content_range << "\n";
+        _status_entry._status_code = SC_invalid_http;
+        return false;
+      }
+    }
+
+  } else {
+    _first_byte_delivered = 0;
+    _last_byte_delivered = 0;
+  }
+  if (downloader_cat.is_debug()) {
+    if (_first_byte_requested != 0 || _last_byte_requested != 0 ||
+        _first_byte_delivered != 0 || _last_byte_delivered != 0) {
+      downloader_cat.debug()
+        << _NOTIFY_HTTP_CHANNEL_ID
+        << "Requested byte range " << _first_byte_requested
+        << " to " << _last_byte_delivered
+        << "; server delivers range " << _first_byte_delivered
+        << " to " << _last_byte_delivered
+        << "\n";
+    }
+  }
+
+  // Set the _document_spec to reflect what we just retrieved.
+  _document_spec = DocumentSpec(_request.get_url());
+  std::string tag = get_header_value("ETag");
+  if (!tag.empty()) {
+    _document_spec.set_tag(HTTPEntityTag(tag));
+  }
+  std::string date = get_header_value("Last-Modified");
+  if (!date.empty()) {
+    _document_spec.set_date(HTTPDate(date));
+  }
+
+  // In case we've got a download in effect, now we know what the first byte
+  // of the subdocument request will be, so we can open the file and position
+  // it.
+  if (status_code / 100 == 1 || status_code == 204 || status_code == 304) {
+    // Never mind on the download.
+    reset_download_to();
+  }
+
+  if (!open_download_file()) {
+    return false;
+  }
+
+  if (_download_dest == DD_ram) {
+    std::string *dest = &_download_to_ramfile->_data;
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      var loaded = 0;
+      xhr.onprogress = function (ev) {
+        var chunk = this.responseText.slice(loaded, ev.loaded);
+        var ptr = __extend_string($1, chunk.length);
+        writeAsciiToMemory(chunk, ptr, true);
+        loaded = ev.loaded;
+      };
+    }, this, dest);
+  }
+  else if (_download_dest == DD_stream) {
+    std::ostream *dest = _download_to_stream;
+    char buffer[4096];
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      var loaded = 0;
+      xhr.onprogress = function (ev) {
+        while (loaded < ev.loaded) {
+          var size = Math.min(ev.loaded - read, 4096);
+          writeAsciiToMemory(this.responseText.substr(read, size), $2, true);
+          __write_stream($1, $2, size);
+          loaded += size;
+        }
+      };
+    }, this, dest, buffer);
+  }
+
+  _got_expected_file_size = false;
+  _got_file_size = false;
+  _got_transfer_file_size = false;
+
+  std::string content_length = get_header_value("Content-Length");
+  if (!content_length.empty()) {
+    _file_size = atoi(content_length.c_str());
+    _got_file_size = true;
+  }
+  else if (status_code == 206) {
+    // Well, we didn't get a content-length from the server, but we can infer
+    // the number of bytes based on the range we're given.
+    _file_size = _last_byte_delivered - _first_byte_delivered + 1;
+    _got_file_size = true;
+  }
+
+  // Reset these for the next request.
+  clear_extra_headers();
+
+  return (_download_dest != DD_none);
+}
+
+/**
+ * Begins a new document request to the server, throwing away whatever request
+ * was currently pending if necessary.
+ */
+bool HTTPChannel::
+begin_request(HTTPEnum::Method method, const DocumentSpec &url,
+              const std::string &body, bool nonblocking,
+              size_t first_byte, size_t last_byte) {
+
+  downloader_cat.info()
+    << _NOTIFY_HTTP_CHANNEL_ID
+    << "begin " << method << " " << url << "\n";
+
+  reset_for_new_request();
+
+  _wanted_nonblocking = nonblocking;
+  _nonblocking = nonblocking || emscripten_has_asyncify();
+
+  _request = url;
+  _document_spec = DocumentSpec();
+  _method = method;
+  _body = body;
+
+  _first_byte_requested = first_byte;
+  _last_byte_requested = last_byte;
+
+  bool result = (bool)EM_ASM_INT({
+    var methods = (["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"]);
+    var xhr = window._httpChannels[$0];
+    try {
+      xhr.open(methods[$1], UTF8ToString($2), !!$3);
+      xhr.withCredentials = true;
+      xhr.overrideMimeType("text/plain; charset=x-user-defined");
+      if ($4 != 0 || $5 != 0) {
+        xhr.setRequestHeader("Range", "bytes=" + $4 + "-" + ($5 || ""));
+      }
+      xhr.onprogress = null;
+      xhr.onreadystatechange = null;
+      return 1;
+    }
+    catch (ex) {
+      (console.error || console.log)(ex);
+      return 0;
+    }
+  }, this, method, _request.get_url().c_str(), (int)_nonblocking, (int)first_byte, (int)last_byte);
+
+  if (!result) {
+    return false;
+  }
+
+  if (_wanted_nonblocking) {
+    // Call run() automatically when the state changes.
+    EM_ASM({
+      var xhr = window._httpChannels[$0];
+      xhr.onreadystatechange = function () {
+        var xhr = window._httpChannels[$0];
+        if (!xhr || !__http_channel_run($0)) {
+          xhr.onreadystatechange = null;
+        }
+      };
+    }, this);
+  }
+  else {
+    return run_send() && run_headers_received();
+  }
+
+  return true;
+}
+
+/**
+ * Resets the internal state variables in preparation for beginning a new
+ * request.
+ */
+void HTTPChannel::
+reset_for_new_request() {
+  if (downloader_cat.is_spam()) {
+    downloader_cat.spam()
+      << _NOTIFY_HTTP_CHANNEL_ID
+      << "reset_for_new_request.\n";
+  }
+
+  EM_ASM({
+    var xhr = window._httpChannels[$0];
+    xhr.onprogress = null;
+    xhr.onreadystatechange = null;
+    if (xhr.readyState !== 0) {
+      try {
+        xhr.abort();
+      }
+      catch (ex) {
+      }
+    }
+  }, this);
+
+  reset_download_to();
+
+  _status_entry = StatusEntry();
+
+  _bytes_downloaded = 0;
+  _bytes_requested = 0;
+}
+
+/**
+ * If a download has been requested, opens the file on disk (or prepares the
+ * RamFile or stream) and seeks within it to the appropriate
+ * _first_byte_delivered position, so that downloaded bytes will be written to
+ * the appropriate point within the file.  Returns true if the starting
+ * position is valid, false otherwise (in which case the state is set to
+ * S_failure).
+ */
+bool HTTPChannel::
+open_download_file() {
+  _subdocument_resumes = (_subdocument_resumes && _first_byte_delivered != 0);
+
+  if (_subdocument_resumes) {
+    if (_download_dest == DD_ram) {
+      if (_first_byte_delivered > _download_to_ramfile->_data.length()) {
+        downloader_cat.info()
+          << _NOTIFY_HTTP_CHANNEL_ID
+          << "Invalid starting position of byte " << _first_byte_delivered
+          << " within Ramfile (which has "
+          << _download_to_ramfile->_data.length() << " bytes)\n";
+        close_download_stream();
+        _status_entry._status_code = SC_download_invalid_range;
+        return false;
+      }
+
+      if (_first_byte_delivered == 0) {
+        _download_to_ramfile->_data = string();
+      }
+      else {
+        _download_to_ramfile->_data =
+          _download_to_ramfile->_data.substr(0, _first_byte_delivered);
+      }
+    }
+    else if (_download_dest == DD_stream) {
+      _download_to_stream->seekp(_first_byte_delivered);
+    }
+  }
+  else {
+    // If _subdocument_resumes is false, we should be sure to reset to the
+    // beginning of the file, regardless of the value of
+    // _first_byte_delivered.
+    if (_download_dest == DD_file || _download_dest == DD_stream) {
+      _download_to_stream->seekp(0);
+    }
+    else if (_download_dest == DD_ram) {
+      _download_to_ramfile->_data = string();
+    }
+  }
+
+  return true;
+}
+
+/**
+ * Interprets the "Content-Range" header in the reply, and fills in
+ * _first_byte_delivered and _last_byte_delivered appropriately if the header
+ * response can be understood.
+ */
+bool HTTPChannel::
+parse_content_range(const std::string &content_range) {
+  // First, get the units indication.
+  size_t p = 0;
+  while (p < content_range.length() && !isspace(content_range[p])) {
+    p++;
+  }
+
+  std::string units = content_range.substr(0, p);
+  while (p < content_range.length() && isspace(content_range[p])) {
+    p++;
+  }
+
+  if (units == "bytes") {
+    const char *c_str = content_range.c_str();
+    char *endptr;
+    if (p < content_range.length() && isdigit(content_range[p])) {
+      long first_byte = strtol(c_str + p, &endptr, 10);
+      p = endptr - c_str;
+      if (p < content_range.length() && content_range[p] == '-') {
+        p++;
+        if (p < content_range.length() && isdigit(content_range[p])) {
+          long last_byte = strtol(c_str + p, &endptr, 10);
+          p = endptr - c_str;
+
+          if (last_byte >= first_byte) {
+            _first_byte_delivered = first_byte;
+            _last_byte_delivered = last_byte;
+            return true;
+          }
+        }
+      }
+    }
+  }
+
+  // Invalid or unhandled response.
+  return false;
+}
+
+/**
+ * Resets the indication of how the document will be downloaded.  This must be
+ * re-specified after each get_document() (or related) call.
+ */
+void HTTPChannel::
+reset_download_to() {
+  close_download_stream();
+  _download_dest = DD_none;
+}
+
+/**
+ * Ensures the file opened for receiving the download has been correctly
+ * closed.
+ */
+void HTTPChannel::
+close_download_stream() {
+  if (_download_to_stream != nullptr) {
+    _download_to_stream->flush();
+    if (_download_dest == DD_file) {
+      VirtualFileSystem::close_write_file(_download_to_stream);
+    }
+  }
+  _download_to_ramfile = nullptr;
+  _download_to_stream = nullptr;
+}
+
+/**
+ * Specifies a Ramfile object to download the resulting document to.  This
+ * should be called immediately after get_document() or begin_get_document()
+ * or related functions.
+ *
+ * In the case of the blocking I/O methods like get_document(), this function
+ * will download the entire document to the Ramfile and return true if it was
+ * successfully downloaded, false otherwise.
+ *
+ * In the case of non-blocking I/O methods like begin_get_document(), this
+ * function simply indicates an intention to download to the indicated
+ * Ramfile.  It returns true if the file can be opened for writing, false
+ * otherwise, but the contents will not be completely downloaded until run()
+ * has returned false.  At this time, it is possible that a communications
+ * error will have left a partial file, so is_download_complete() may be
+ * called to test this.
+ *
+ * If subdocument_resumes is true and the document in question was previously
+ * requested as a subdocument (i.e.  get_subdocument() with a first_byte value
+ * greater than zero), this will automatically seek to the appropriate byte
+ * within the Ramfile for writing the output.  In this case, the Ramfile must
+ * already have at least first_byte bytes in it.
+ */
+bool HTTPChannel::
+download_to_ram(Ramfile *ramfile, bool subdocument_resumes) {
+  State state = get_state();
+  nassertr(state != S_unsent, false);
+  nassertr(ramfile != nullptr, false);
+
+  reset_download_to();
+  ramfile->_pos = 0;
+  _download_dest = DD_ram;
+  _download_to_ramfile = ramfile;
+  _subdocument_resumes = (subdocument_resumes && _first_byte_delivered != 0);
+
+  if (state != S_done && _wanted_nonblocking) {
+    // In nonblocking mode, we just kick off the request.
+    return state != S_opened || run_send();
+  }
+
+  // In normal, blocking mode, go ahead and do the download.
+  if (!open_download_file()) {
+    reset_download_to();
+    return false;
+  }
+
+  if (state != S_done) {
+    while (run()) {
+    }
+  }
+
+  // Copy the entire response text.
+  int bytes_read = EM_ASM_INT({
+    var xhr = window._httpChannels[$0];
+    var state = xhr.readyState;
+    var body = xhr.responseText;
+    var ptr = __extend_string($1, body.length);
+    writeAsciiToMemory(body, ptr, true);
+    return state;
+  }, this, &ramfile->_data);
+
+  _bytes_downloaded = bytes_read;
+
+  close_download_stream();
+
+  return is_valid();
+}
+
+/**
+ * Specifies the name of an ostream to download the resulting document to.
+ * This should be called immediately after get_document() or
+ * begin_get_document() or related functions.
+ *
+ * In the case of the blocking I/O methods like get_document(), this function
+ * will download the entire document to the file and return true if it was
+ * successfully downloaded, false otherwise.
+ *
+ * In the case of non-blocking I/O methods like begin_get_document(), this
+ * function simply indicates an intention to download to the indicated file.
+ * It returns true if the file can be opened for writing, false otherwise, but
+ * the contents will not be completely downloaded until run() has returned
+ * false.  At this time, it is possible that a communications error will have
+ * left a partial file, so is_download_complete() may be called to test this.
+ *
+ * If subdocument_resumes is true and the document in question was previously
+ * requested as a subdocument (i.e.  get_subdocument() with a first_byte value
+ * greater than zero), this will automatically seek to the appropriate byte
+ * within the file for writing the output.  In this case, the file must
+ * already exist and must have at least first_byte bytes in it.  If
+ * subdocument_resumes is false, a subdocument will always be downloaded
+ * beginning at the first byte of the file.
+ */
+bool HTTPChannel::
+download_to_stream(std::ostream *strm, bool subdocument_resumes) {
+  State state = get_state();
+  nassertr(state != S_unsent, false);
+
+  reset_download_to();
+  _download_dest = DD_stream;
+  _download_to_stream = strm;
+  _download_to_stream->clear();
+  _subdocument_resumes = subdocument_resumes;
+
+  if (state != S_done && _wanted_nonblocking) {
+    // In nonblocking mode, we just kick off the request.
+    return state != S_opened || run_send();
+  }
+
+  // In normal, blocking mode, go ahead and do the download.
+  if (!open_download_file()) {
+    reset_download_to();
+    return false;
+  }
+
+  if (state != S_done) {
+    while (run()) {
+    }
+  }
+
+  // Copy the entire response text.
+  char buffer[4096];
+  int bytes_read = EM_ASM_INT({
+    var xhr = window._httpChannels[$0];
+    var state = xhr.readyState;
+    var body = xhr.responseText;
+    var read = 0;
+    while (read < body.length) {
+      var size = Math.min(body.length - read, 4096);
+      for (var dest = $2; dest < $2 + size; ++dest) {
+        HEAP8[(dest>>0)] = body.charCodeAt(read++) & 0xff;
+      }
+      __write_stream($1, $2, size);
+    }
+    return read;
+  }, this, strm, buffer);
+
+  strm->flush();
+  _bytes_downloaded = bytes_read;
+
+  close_download_stream();
+
+  return is_valid();
+}
+
+#endif  // __EMSCRIPTEN__

+ 241 - 0
panda/src/downloader/httpChannel_emscripten.h

@@ -0,0 +1,241 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpChannel_emscripten.h
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#ifndef HTTPCHANNEL_EMSCRIPTEN_H
+#define HTTPCHANNEL_EMSCRIPTEN_H
+
+#include "pandabase.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "config_downloader.h"
+#include "documentSpec.h"
+#include "httpEnum.h"
+#include "pmap.h"
+#include "pointerTo.h"
+#include "pvector.h"
+#include "ramfile.h"
+#include "typedReferenceCount.h"
+#include "urlSpec.h"
+
+class HTTPClient;
+
+/**
+ * This is a reduced implementation of HTTPChannel used on the web, which uses
+ * XMLHttpRequest instead of OpenSSL.  It offers fewer features.
+ */
+class EXPCL_PANDA_DOWNLOADER HTTPChannel : public TypedReferenceCount {
+private:
+  HTTPChannel(HTTPClient *client);
+
+public:
+  virtual ~HTTPChannel();
+
+PUBLISHED:
+  // get_status_code() will either return an HTTP-style status code >= 100
+  // (e.g.  404), or one of the following values.  In general, these are
+  // ordered from less-successful to more-successful.
+  enum StatusCode {
+    SC_incomplete = 0,
+    SC_internal_error,
+    SC_no_connection,
+    SC_timeout,
+    SC_lost_connection,
+    SC_non_http_response,
+    SC_invalid_http,
+    SC_socks_invalid_version,
+    SC_socks_no_acceptable_login_method,
+    SC_socks_refused,
+    SC_socks_no_connection,
+    SC_ssl_internal_failure,
+    SC_ssl_no_handshake,
+
+    // No one returns this code, but StatusCode values higher than this are
+    // deemed more successful than any generic HTTP response.
+    SC_http_error_watermark,
+
+    SC_ssl_invalid_server_certificate,
+    SC_ssl_self_signed_server_certificate,
+    SC_ssl_unexpected_server,
+
+    // These errors are only generated after a download_to_*() call been
+    // issued.
+    SC_download_open_error,
+    SC_download_write_error,
+    SC_download_invalid_range,
+  };
+
+  INLINE HTTPClient *get_client() const;
+
+  INLINE bool is_valid() const;
+
+  INLINE const URLSpec &get_url() const;
+  INLINE const DocumentSpec &get_document_spec() const;
+  INLINE int get_status_code() const;
+  INLINE std::string get_status_string() const;
+  INLINE const std::string &get_www_realm() const;
+  std::string get_header_value(const std::string &key) const;
+
+  INLINE void set_content_type(std::string content_type);
+  INLINE std::string get_content_type() const;
+
+  INLINE void set_expected_file_size(size_t file_size);
+  std::streamsize get_file_size() const;
+  INLINE bool is_file_size_known() const;
+
+  INLINE size_t get_first_byte_requested() const;
+  INLINE size_t get_last_byte_requested() const;
+  INLINE size_t get_first_byte_delivered() const;
+  INLINE size_t get_last_byte_delivered() const;
+
+  void write_headers(std::ostream &out) const;
+
+  INLINE void reset();
+  INLINE void preserve_status();
+
+  INLINE void clear_extra_headers();
+  INLINE void send_extra_header(const std::string &key, const std::string &value);
+
+  BLOCKING INLINE bool get_document(const DocumentSpec &url);
+  BLOCKING INLINE bool get_subdocument(const DocumentSpec &url,
+                                       size_t first_byte, size_t last_byte);
+  BLOCKING INLINE bool get_header(const DocumentSpec &url);
+  BLOCKING INLINE bool post_form(const DocumentSpec &url, const std::string &body);
+  BLOCKING INLINE bool put_document(const DocumentSpec &url, const std::string &body);
+  BLOCKING INLINE bool delete_document(const DocumentSpec &url);
+  BLOCKING INLINE bool get_options(const DocumentSpec &url);
+
+  INLINE void begin_get_document(const DocumentSpec &url);
+  INLINE void begin_get_subdocument(const DocumentSpec &url,
+                                    size_t first_byte, size_t last_byte);
+  INLINE void begin_get_header(const DocumentSpec &url);
+  INLINE void begin_post_form(const DocumentSpec &url, const std::string &body);
+  bool run();
+
+  BLOCKING bool download_to_ram(Ramfile *ramfile, bool subdocument_resumes = true);
+  BLOCKING bool download_to_stream(std::ostream *strm, bool subdocument_resumes = true);
+
+  INLINE size_t get_bytes_downloaded() const;
+  INLINE size_t get_bytes_requested() const;
+  INLINE bool is_download_complete() const;
+
+private:
+  enum State {
+    S_unsent = 0,
+    S_opened = 1,
+    S_headers_received = 2,
+    S_loading = 3,
+    S_done = 4
+  };
+  State get_state() const;
+
+  bool run_send();
+  bool run_headers_received();
+
+  bool begin_request(HTTPEnum::Method method, const DocumentSpec &url,
+                     const std::string &body, bool nonblocking,
+                     size_t first_byte, size_t last_byte);
+  void reset_for_new_request();
+
+  bool open_download_file();
+
+  bool parse_content_range(const std::string &content_range);
+
+  void reset_download_to();
+  void close_download_stream();
+
+private:
+  class StatusEntry {
+  public:
+    INLINE StatusEntry();
+    int _status_code;
+    std::string _status_string;
+  };
+  typedef pvector<StatusEntry> StatusList;
+
+  HTTPClient *_client;
+  StatusList _status_list;
+  URLSpec _proxy;
+
+  typedef std::pair<std::string, std::string> ExtraHeader;
+  pvector<ExtraHeader> _send_extra_headers;
+
+  bool _nonblocking;
+  bool _wanted_nonblocking;
+
+  DocumentSpec _document_spec;
+  DocumentSpec _request;
+  HTTPEnum::Method _method;
+  std::string request_path;
+  std::string _header;
+  std::string _body;
+  std::string _content_type;
+  size_t _first_byte_requested;
+  size_t _last_byte_requested;
+  size_t _first_byte_delivered;
+  size_t _last_byte_delivered;
+
+  enum DownloadDest {
+    DD_none,
+    DD_file,
+    DD_ram,
+    DD_stream,
+  };
+  DownloadDest _download_dest;
+  bool _subdocument_resumes;
+  //Filename _download_to_filename;
+  Ramfile *_download_to_ramfile;
+  std::ostream *_download_to_stream;
+
+  StatusEntry _status_entry;
+
+  std::string _www_realm;
+  //std::string _www_username;
+  //PT(HTTPAuthorization) _www_auth;
+
+  typedef pmap<std::string, std::string> Headers;
+  Headers _headers;
+
+  size_t _expected_file_size;
+  size_t _file_size;
+  size_t _transfer_file_size;
+  size_t _bytes_downloaded;
+  size_t _bytes_requested;
+  bool _got_expected_file_size;
+  bool _got_file_size;
+  bool _got_transfer_file_size;
+
+public:
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    TypedReferenceCount::init_type();
+    register_type(_type_handle, "HTTPChannel",
+                  TypedReferenceCount::get_class_type());
+  }
+
+private:
+  static TypeHandle _type_handle;
+  friend class HTTPClient;
+};
+
+#include "httpChannel_emscripten.I"
+
+#endif  // __EMSCRIPTEN__
+
+#endif

+ 4 - 0
panda/src/downloader/httpClient.h

@@ -214,6 +214,10 @@ private:
 
 #include "httpClient.I"
 
+#elif defined(__EMSCRIPTEN__)
+
+#include "httpClient_emscripten.h"
+
 #endif  // HAVE_OPENSSL
 
 #endif

+ 32 - 0
panda/src/downloader/httpClient_emscripten.I

@@ -0,0 +1,32 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpClient_emscripten.I
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+/**
+ * Implements HTTPAuthorization::base64_encode().  This is provided here just
+ * as a convenient place to publish it for access by the scripting language;
+ * C++ code should probably use HTTPAuthorization directly.
+ */
+INLINE std::string HTTPClient::
+base64_encode(const std::string &s) {
+  return HTTPAuthorization::base64_encode(s);
+}
+
+/**
+ * Implements HTTPAuthorization::base64_decode().  This is provided here just
+ * as a convenient place to publish it for access by the scripting language;
+ * C++ code should probably use HTTPAuthorization directly.
+ */
+INLINE std::string HTTPClient::
+base64_decode(const std::string &s) {
+  return HTTPAuthorization::base64_decode(s);
+}

+ 424 - 0
panda/src/downloader/httpClient_emscripten.cxx

@@ -0,0 +1,424 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpClient_emscripten.cxx
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#include "httpClient_emscripten.h"
+#include "httpChannel.h"
+#include "config_downloader.h"
+#include "httpBasicAuthorization.h"
+#include "httpDigestAuthorization.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include <emscripten/em_asm.h>
+
+using std::string;
+
+PT(HTTPClient) HTTPClient::_global_ptr;
+
+/**
+ *
+ */
+HTTPClient::
+HTTPClient() {
+  ConfigVariableList http_username
+    ("http-username",
+     PRC_DESC("Adds one or more username/password pairs to all HTTP clients.  The client "
+              "will present this username/password when asked to authenticate a request "
+              "for a particular server and/or realm.  The username is of the form "
+              "server:realm:username:password, where either or both of server and "
+              "realm may be empty, or just realm:username:password or username:password.  "
+              "If the server or realm is empty, they will match anything."));
+
+  {
+    // Also load in the general usernames.
+    int num_unique_values = http_username.get_num_unique_values();
+    for (int i = 0; i < num_unique_values; i++) {
+      string username = http_username.get_unique_value(i);
+      add_http_username(username);
+    }
+  }
+}
+
+/**
+ * Specifies the username:password string corresponding to a particular server
+ * and/or realm, when demanded by the server.  Either or both of the server or
+ * realm may be empty; if so, they match anything.  Also, the server may be
+ * set to the special string `"*proxy"`, which will match any proxy server.
+ *
+ * If the username is set to the empty string, this clears the password for
+ * the particular server/realm pair.
+ */
+void HTTPClient::
+set_username(const string &server, const string &realm, const string &username) {
+  string key = server + ":" + realm;
+  if (username.empty()) {
+    _usernames.erase(key);
+  } else {
+    _usernames[key] = username;
+  }
+}
+
+/**
+ * Returns the username:password string set for this server/realm pair, or
+ * empty string if nothing has been set.  See set_username().
+ */
+string HTTPClient::
+get_username(const string &server, const string &realm) const {
+  string key = server + ":" + realm;
+  Usernames::const_iterator ui;
+  ui = _usernames.find(key);
+  if (ui != _usernames.end()) {
+    return (*ui).second;
+  }
+  return string();
+}
+
+/**
+ * Stores the indicated cookie in the client's list of cookies, as if it had
+ * been received from a server.
+ */
+void HTTPClient::
+set_cookie(const HTTPCookie &cookie) {
+  std::ostringstream stream;
+  stream << cookie;
+  std::string str = stream.str();
+
+  EM_ASM({
+    document.cookie = UTF8ToString($0);
+  }, str.c_str());
+}
+
+/**
+ * Removes the cookie with the matching domain/path/name from the client's
+ * list of cookies.  Returns true if it was removed, false if the cookie was
+ * not matched.
+ */
+bool HTTPClient::
+clear_cookie(const HTTPCookie &cookie) {
+  HTTPCookie expired;
+  expired.set_name(cookie.get_name());
+  expired.set_path(cookie.get_path());
+  expired.set_domain(cookie.get_domain());
+  expired.set_expires(HTTPDate((time_t)0));
+
+  std::ostringstream stream;
+  stream << expired;
+  std::string str = stream.str();
+
+  return (bool)EM_ASM_INT({
+    var set = UTF8ToString($0);
+    var old = document.cookie;
+    document.cookie = set;
+    return (document.cookie !== old);
+  }, str.c_str());
+}
+
+/**
+ * Removes the all stored cookies from the client.
+ */
+void HTTPClient::
+clear_all_cookies() {
+  // NB. This is imperfect, and won't clear cookies with other domains or paths.
+  EM_ASM({
+    var cookies = document.cookie.split(";");
+    for (var i = 0; i < cookies.length; ++i) {
+      var name = cookies[i].split("=", 1)[0];
+      document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+    }
+
+    cookies = document.cookie.split(";");
+    for (var i = 0; i < cookies.length; ++i) {
+      var name = cookies[i].split("=", 1)[0];
+      document.cookie = name + "=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+    }
+  });
+}
+
+/**
+ * Returns true if there is a cookie in the client matching the given cookie's
+ * domain/path/name, false otherwise.
+ */
+//bool HTTPClient::
+//has_cookie(const HTTPCookie &cookie) const {
+//}
+
+/**
+ * Looks up and returns the cookie in the client matching the given cookie's
+ * domain/path/name.  If there is no matching cookie, returns an empty cookie.
+ */
+//HTTPCookie HTTPClient::
+//get_cookie(const HTTPCookie &cookie) const {
+//  return HTTPCookie();
+//}
+
+/**
+ * Outputs the complete list of cookies stored on the client, for all domains,
+ * including the expired cookies (which will normally not be sent back to a
+ * host).
+ */
+void HTTPClient::
+write_cookies(std::ostream &out) const {
+  char *str = (char *)EM_ASM_INT({
+    var str = document.cookie.replace(/; ?/g, "\n");
+    var len = lengthBytesUTF8(str) + 1;
+    var buffer = _malloc(len);
+    stringToUTF8(str, buffer, len);
+    return buffer;
+  });
+
+  out << str << "\n";
+  free(str);
+}
+
+/**
+ * Returns a new HTTPChannel object that may be used for reading multiple
+ * documents using the same connection, for greater network efficiency than
+ * calling HTTPClient::get_document() repeatedly (which would force a new
+ * connection for each document).
+ *
+ * Also, HTTPChannel has some additional, less common interface methods than
+ * the basic interface methods that exist on HTTPClient; if you wish to call
+ * any of these methods you must first obtain an HTTPChannel.
+ */
+PT(HTTPChannel) HTTPClient::
+make_channel(bool persistent_connection) {
+  return new HTTPChannel(this);
+}
+
+/**
+ * Posts form data to a particular URL and retrieves the response.  Returns a
+ * new HTTPChannel object whether the document is successfully read or not;
+ * you can test is_valid() and get_return_code() to determine whether the
+ * document was retrieved.
+ */
+PT(HTTPChannel) HTTPClient::
+post_form(const URLSpec &url, const string &body) {
+  PT(HTTPChannel) doc = new HTTPChannel(this);
+  doc->post_form(url, body);
+  return doc;
+}
+
+/**
+ * Opens the named document for reading.  Returns a new HTTPChannel object
+ * whether the document is successfully read or not; you can test is_valid()
+ * and get_return_code() to determine whether the document was retrieved.
+ */
+PT(HTTPChannel) HTTPClient::
+get_document(const URLSpec &url) {
+  PT(HTTPChannel) doc = new HTTPChannel(this);
+  doc->get_document(url);
+  return doc;
+}
+
+/**
+ * Like get_document(), except only the header associated with the document is
+ * retrieved.  This may be used to test for existence of the document; it
+ * might also return the size of the document (if the server gives us this
+ * information).
+ */
+PT(HTTPChannel) HTTPClient::
+get_header(const URLSpec &url) {
+  PT(HTTPChannel) doc = new HTTPChannel(this);
+  doc->get_header(url);
+  return doc;
+}
+
+/**
+ * Returns the default global HTTPClient.
+ */
+HTTPClient *HTTPClient::
+get_global_ptr() {
+  if (_global_ptr == nullptr) {
+    _global_ptr = new HTTPClient;
+  }
+  return _global_ptr;
+}
+
+/**
+ * Handles a Config definition for http-username as
+ * server:realm:username:password, where either or both of server and realm
+ * may be empty, or just server:username:password or username:password.
+ */
+void HTTPClient::
+add_http_username(const string &http_username) {
+  size_t c1 = http_username.find(':');
+  if (c1 != string::npos) {
+    size_t c2 = http_username.find(':', c1 + 1);
+    if (c2 != string::npos) {
+      size_t c3 = http_username.find(':', c2 + 1);
+      if (c3 != string::npos) {
+        size_t c4 = http_username.find(':', c3 + 1);
+        if (c4 != string::npos) {
+          // Oops, we have five?  Problem.
+          downloader_cat.error()
+            << "Invalid http-username " << http_username << "\n";
+        }
+        else {
+          // Ok, we have four.
+          set_username(http_username.substr(0, c1),
+                       http_username.substr(c1 + 1, c2 - (c1 + 1)),
+                       http_username.substr(c2 + 1));
+        }
+      }
+      else {
+        // We have only three.
+        set_username(string(),
+                     http_username.substr(0, c1),
+                     http_username.substr(c1 + 1));
+      }
+    }
+    else {
+      // We have only two.
+      set_username(string(), string(), http_username);
+    }
+  } else {
+    // We have only one?  Problem.
+    downloader_cat.error()
+      << "Invalid http-username " << http_username << "\n";
+  }
+}
+
+/**
+ * Chooses a suitable username:password string for the given URL and realm.
+ */
+string HTTPClient::
+select_username(const URLSpec &url, const string &realm) const {
+  string username;
+
+  // Look in several places in order to find the matching username.
+
+  // Fist, if there's a username on the URL, that always wins (except when we
+  // are looking for a proxy username).
+  if (url.has_username()) {
+    username = url.get_username();
+  }
+
+  // Otherwise, start looking on the HTTPClient.
+  if (username.empty()) {
+    // Try the specific serverrealm.
+    username = get_username(url.get_server(), realm);
+  }
+  if (username.empty()) {
+    // Then, try the specific serverany realm.
+    username = get_username(url.get_server(), string());
+  }
+  if (username.empty()) {
+    // Then, try any server with this realm.
+    username = get_username(string(), realm);
+  }
+  if (username.empty()) {
+    // Then, take the general password.
+    username = get_username(string(), string());
+  }
+
+  return username;
+}
+
+/**
+ * Chooses a suitable pre-computed authorization for the indicated URL.
+ * Returns NULL if no authorization matches.
+ */
+HTTPAuthorization *HTTPClient::
+select_auth(const URLSpec &url, const string &last_realm) {
+  Domains &domains = _www_domains;
+  std::string canon = HTTPAuthorization::get_canonical_url(url).get_url();
+
+  // Look for the longest domain string that is a prefix of our canonical URL.
+  // We have to make a linear scan through the list.
+  Domains::const_iterator best_di = domains.end();
+  size_t longest_length = 0;
+  Domains::const_iterator di;
+  for (di = domains.begin(); di != domains.end(); ++di) {
+    const string &domain = (*di).first;
+    size_t length = domain.length();
+    if (domain == canon.substr(0, length)) {
+      // This domain string matches.  Is it the longest?
+      if (length > longest_length) {
+        best_di = di;
+        longest_length = length;
+      }
+    }
+  }
+
+  if (best_di != domains.end()) {
+    // Ok, we found a matching domain.  Use it.
+    if (downloader_cat.is_spam()) {
+      downloader_cat.spam()
+        << "Choosing domain " << (*best_di).first << " for " << url << "\n";
+    }
+    const Realms &realms = (*best_di).second._realms;
+    // First, try our last realm.
+    Realms::const_iterator ri;
+    ri = realms.find(last_realm);
+    if (ri != realms.end()) {
+      return (*ri).second;
+    }
+
+    if (!realms.empty()) {
+      // Oh well, just return the first realm.
+      return (*realms.begin()).second;
+    }
+  }
+
+  // No matching domains.
+  return nullptr;
+}
+
+/**
+ * Generates a new authorization entry in response to a 401 or 407 challenge
+ * from the server or proxy.  The new authorization entry is stored for future
+ * connections to the same server (or, more precisely, the same domain, which
+ * may be a subset of the server, or it may include multiple servers).
+ */
+PT(HTTPAuthorization) HTTPClient::
+generate_auth(const URLSpec &url, const string &challenge) {
+  HTTPAuthorization::AuthenticationSchemes schemes;
+  HTTPAuthorization::parse_authentication_schemes(schemes, challenge);
+
+  PT(HTTPAuthorization) auth;
+  HTTPAuthorization::AuthenticationSchemes::iterator si;
+
+  si = schemes.find("digest");
+  if (si != schemes.end()) {
+    auth = new HTTPDigestAuthorization((*si).second, url, false);
+  }
+
+  if (auth == nullptr || !auth->is_valid()) {
+    si = schemes.find("basic");
+    if (si != schemes.end()) {
+      auth = new HTTPBasicAuthorization((*si).second, url, false);
+    }
+  }
+
+  if (auth == nullptr || !auth->is_valid()) {
+    downloader_cat.warning()
+      << "Don't know how to use any of the server's available authorization schemes:\n";
+    for (si = schemes.begin(); si != schemes.end(); ++si) {
+      downloader_cat.warning() << (*si).first << "\n";
+    }
+  }
+  else {
+    // Now that we've got an authorization, store it under under each of its
+    // suggested domains for future use.
+    Domains &domains = _www_domains;
+    const vector_string &domain = auth->get_domain();
+    vector_string::const_iterator si;
+    for (si = domain.begin(); si != domain.end(); ++si) {
+      domains[(*si)]._realms[auth->get_realm()] = auth;
+    }
+  }
+
+  return auth;
+}
+
+#endif  // __EMSCRIPTEN__

+ 93 - 0
panda/src/downloader/httpClient_emscripten.h

@@ -0,0 +1,93 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file httpClient_emscripten.h
+ * @author rdb
+ * @date 2021-02-10
+ */
+
+#ifndef HTTPCLIENT_EMSCRIPTEN_H
+#define HTTPCLIENT_EMSCRIPTEN_H
+
+#include "pandabase.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "urlSpec.h"
+#include "httpAuthorization.h"
+#include "httpEnum.h"
+#include "httpChannel.h"
+#include "httpCookie.h"
+#include "pointerTo.h"
+#include "pvector.h"
+#include "pmap.h"
+#include "referenceCount.h"
+
+class Filename;
+class HTTPChannel;
+
+/**
+ * Reduced version of HTTPClient that is available in Emscripten.  It uses the
+ * browser to make HTTP requests.  As such, there is only a global HTTPClient
+ * pointer, and it is not possible to make individual HTTPClient objects.
+ */
+class EXPCL_PANDA_DOWNLOADER HTTPClient : public ReferenceCount {
+private:
+  HTTPClient();
+
+PUBLISHED:
+  void set_username(const std::string &server, const std::string &realm, const std::string &username);
+  std::string get_username(const std::string &server, const std::string &realm) const;
+
+  void set_cookie(const HTTPCookie &cookie);
+  bool clear_cookie(const HTTPCookie &cookie);
+  void clear_all_cookies();
+  //bool has_cookie(const HTTPCookie &cookie) const;
+  //HTTPCookie get_cookie(const HTTPCookie &cookie) const;
+
+  void write_cookies(std::ostream &out) const;
+
+  PT(HTTPChannel) make_channel(bool persistent_connection);
+  BLOCKING PT(HTTPChannel) post_form(const URLSpec &url, const std::string &body);
+  BLOCKING PT(HTTPChannel) get_document(const URLSpec &url);
+  BLOCKING PT(HTTPChannel) get_header(const URLSpec &url);
+
+  INLINE static std::string base64_encode(const std::string &s);
+  INLINE static std::string base64_decode(const std::string &s);
+
+  static HTTPClient *get_global_ptr();
+
+private:
+  void add_http_username(const std::string &http_username);
+  std::string select_username(const URLSpec &url, const std::string &realm) const;
+
+  HTTPAuthorization *select_auth(const URLSpec &url, const std::string &last_realm);
+  PT(HTTPAuthorization) generate_auth(const URLSpec &url,
+                                      const std::string &challenge);
+
+  typedef pmap<std::string, std::string> Usernames;
+  Usernames _usernames;
+
+  typedef pmap<std::string, PT(HTTPAuthorization)> Realms;
+  class Domain {
+  public:
+    Realms _realms;
+  };
+  typedef pmap<std::string, Domain> Domains;
+  Domains _www_domains;
+
+  static PT(HTTPClient) _global_ptr;
+
+  friend class HTTPChannel;
+};
+
+#include "httpClient_emscripten.I"
+
+#endif  // __EMSCRIPTEN__
+
+#endif

+ 1 - 1
panda/src/downloader/httpCookie.cxx

@@ -13,7 +13,7 @@
 
 #include "httpCookie.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 #include "httpChannel.h"
 #include "string_utils.h"

+ 1 - 1
panda/src/downloader/httpCookie.h

@@ -20,7 +20,7 @@
 // this to establish https connections; this is because it uses the OpenSSL
 // library to portably handle all of the socket communications.
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 #include "httpDate.h"
 #include "urlSpec.h"

+ 1 - 1
panda/src/downloader/httpDigestAuthorization.cxx

@@ -13,7 +13,7 @@
 
 #include "httpDigestAuthorization.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 #include "hashVal.h"
 #include "string_utils.h"

+ 1 - 1
panda/src/downloader/httpDigestAuthorization.h

@@ -20,7 +20,7 @@
 // use any OpenSSL code, because it is a support module for HTTPChannel, which
 // *does* use OpenSSL code.
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 #include "httpAuthorization.h"
 

+ 1 - 1
panda/src/downloader/httpEnum.cxx

@@ -13,7 +13,7 @@
 
 #include "httpEnum.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 /**
  *

+ 3 - 1
panda/src/downloader/httpEnum.h

@@ -20,7 +20,7 @@
 // this to establish https connections; this is because it uses the OpenSSL
 // library to portably handle all of the socket communications.
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 /**
  * This class is just used as a namespace wrapper for some of the enumerated
@@ -28,12 +28,14 @@
  */
 class EXPCL_PANDA_DOWNLOADER HTTPEnum {
 PUBLISHED:
+#ifdef HAVE_OPENSSL
   enum HTTPVersion {
     HV_09,  // HTTP 0.9 or older
     HV_10,  // HTTP 1.0
     HV_11,  // HTTP 1.1
     HV_other,
   };
+#endif
 
   enum Method {
     M_options,

+ 2 - 0
panda/src/downloader/p3downloader_composite2.cxx

@@ -1,7 +1,9 @@
 #include "httpAuthorization.cxx"
 #include "httpBasicAuthorization.cxx"
 #include "httpChannel.cxx"
+#include "httpChannel_emscripten.cxx"
 #include "httpClient.cxx"
+#include "httpClient_emscripten.cxx"
 #include "httpCookie.cxx"
 #include "httpDate.cxx"
 #include "httpDigestAuthorization.cxx"

+ 24 - 1
panda/src/downloader/virtualFileHTTP.cxx

@@ -19,7 +19,7 @@
 
 #include <iterator>
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 using std::istream;
 using std::ostream;
@@ -140,9 +140,32 @@ open_read_file(bool auto_unwrap) const {
     return nullptr;
   }
 
+  strstream->seekg(0);
+
   return return_file(strstream, auto_unwrap);
 }
 
+/**
+ * Fills up the indicated string with the contents of the file, if it is a
+ * regular file.  Returns true on success, false otherwise.
+ */
+bool VirtualFileHTTP::
+read_file(string &result, bool auto_unwrap) const {
+  result = string();
+
+  if (_status_only) {
+    return false;
+  }
+
+  Ramfile ramfile;
+  if (!_channel->download_to_ram(&ramfile, false)) {
+    return false;
+  }
+
+  result = std::move(ramfile._data);
+  return true;
+}
+
 /**
  * Fills up the indicated pvector with the contents of the file, if it is a
  * regular file.  Returns true on success, false otherwise.

+ 2 - 1
panda/src/downloader/virtualFileHTTP.h

@@ -20,7 +20,7 @@
 #include "httpChannel.h"
 #include "urlSpec.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 class VirtualFileMountHTTP;
 
@@ -51,6 +51,7 @@ public:
   virtual std::streamsize get_file_size() const;
   virtual time_t get_timestamp() const;
 
+  virtual bool read_file(std::string &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
 
 private:

+ 1 - 1
panda/src/downloader/virtualFileMountHTTP.cxx

@@ -15,7 +15,7 @@
 #include "virtualFileHTTP.h"
 #include "virtualFileSystem.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 using std::string;
 

+ 1 - 1
panda/src/downloader/virtualFileMountHTTP.h

@@ -16,7 +16,7 @@
 
 #include "pandabase.h"
 
-#ifdef HAVE_OPENSSL
+#if defined(HAVE_OPENSSL) || defined(__EMSCRIPTEN__)
 
 #include "virtualFileMount.h"
 #include "httpClient.h"

+ 13 - 0
panda/src/event/asyncTaskManager.cxx

@@ -663,3 +663,16 @@ make_global_ptr() {
   _global_ptr = new AsyncTaskManager("TaskManager");
   _global_ptr->ref();
 }
+
+#ifdef __EMSCRIPTEN__
+
+extern "C" void task_manager_poll();
+
+void task_manager_poll() {
+  AsyncTaskManager *mgr = AsyncTaskManager::get_global_ptr();
+  nassertv_always(mgr != NULL);
+
+  mgr->poll();
+}
+
+#endif

+ 40 - 0
panda/src/express/trueClock.cxx

@@ -473,6 +473,46 @@ set_time_scale(double time, double new_time_scale) {
   _time_scale = new_time_scale;
 }
 
+#elif defined(__EMSCRIPTEN__)
+
+/**
+ * The Emscripten implementation.  This uses either the JavaScript function
+ * performance.now() if available, otherwise Date.now().
+ */
+
+#include <emscripten.h>
+
+/**
+ *
+ */
+double TrueClock::
+get_long_time() {
+  return emscripten_get_now() * 0.001;
+}
+
+/**
+ *
+ */
+double TrueClock::
+get_short_raw_time() {
+  return emscripten_get_now() * 0.001;
+}
+
+/**
+ *
+ */
+bool TrueClock::
+set_cpu_affinity(uint32_t mask) const {
+  return false;
+}
+
+/**
+ *
+ */
+TrueClock::
+TrueClock() {
+}
+
 #else  // !_WIN32
 
 // The Posix implementation.

+ 1 - 1
panda/src/express/virtualFile.h

@@ -84,7 +84,7 @@ public:
   INLINE bool write_file(const std::string &data, bool auto_wrap);
 
   INLINE void set_original_filename(const Filename &filename);
-  bool read_file(std::string &result, bool auto_unwrap) const;
+  virtual bool read_file(std::string &result, bool auto_unwrap) const;
   virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
   virtual bool write_file(const unsigned char *data, size_t data_size, bool auto_wrap);
 

+ 51 - 3
panda/src/express/virtualFileSystem.cxx

@@ -28,6 +28,10 @@
 #include "executionEnvironment.h"
 #include "pset.h"
 
+#ifdef __EMSCRIPTEN__
+#include "virtualFileMountHTTP.h"
+#endif
+
 using std::iostream;
 using std::istream;
 using std::ostream;
@@ -853,10 +857,54 @@ get_global_ptr() {
     _global_ptr = new VirtualFileSystem;
 
     // Set up the default mounts.  First, there is always the root mount.
-    _global_ptr->mount("/", "/", 0);
+#ifdef __EMSCRIPTEN__
+    // Unless we're running in node.js, we don't have a filesystem, and instead
+    // mount the current server root as our filesystem root.
+    bool is_node = (bool)EM_ASM_INT(return (typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string'));
+    if (!is_node) {
+      _global_ptr->mount(new VirtualFileMountHTTP(URLSpec("/")), "/", MF_read_only);
+
+      // And get the "current working directory".
+      char cwd[4096];
+      bool have_memfs = (bool)EM_ASM_INT({
+        var path = location.pathname;
+        stringToUTF8(path.substring(0, path.lastIndexOf('/')), $0, 4096);
+
+        if (FS && FS.root) {
+          /* Emscripten creates these by default, but we don't want them. */
+          var contents = FS.root.contents;
+          delete contents.dev;
+          delete contents.home;
+          delete contents.proc;
+          delete contents.tmp;
+          return true;
+        }
+        else {
+          return false;
+        }
+      }, cwd);
+
+      if (cwd[0] == 0) {
+        cwd[0] = '/';
+        cwd[1] = 0;
+      }
 
-    // And our initial cwd comes from the environment.
-    _global_ptr->chdir(ExecutionEnvironment::get_cwd());
+      _global_ptr->_cwd = cwd;
+
+      // If we built with the Emscripten VFS enabled, mount it on top of the
+      // current directory, so that emscripten's preload system will work.
+      if (have_memfs) {
+        _global_ptr->mount("/", _global_ptr->_cwd, MF_read_only);
+      }
+    }
+    else
+#endif
+    {
+      _global_ptr->mount("/", "/", 0);
+
+      // And our initial cwd comes from the environment.
+      _global_ptr->chdir(ExecutionEnvironment::get_cwd());
+    }
 
     // Then, we add whatever mounts are listed in the Configrc file.
     ConfigVariableList mounts

+ 21 - 1
panda/src/framework/pandaFramework.cxx

@@ -37,8 +37,24 @@
 #endif
 #endif
 
+extern "C" EXPCL_PANDA_PNMIMAGETYPES void init_libpnmimagetypes();
+
 using std::string;
 
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+
+static void em_do_frame(void *arg) {
+  PandaFramework *fwx = (PandaFramework *)arg;
+  nassertv_always(fwx != NULL);
+
+  if (!fwx->do_frame(Thread::get_current_thread())) {
+    emscripten_cancel_main_loop();
+    framework_cat.info() << "Main loop cancelled.\n";
+  }
+}
+#endif
+
 LoaderOptions PandaFramework::_loader_options;
 
 /**
@@ -115,7 +131,6 @@ open_framework() {
 
   // Let's explicitly make a call to the image type library to ensure it gets
   // pulled in by the dynamic linker.
-  extern EXPCL_PANDA_PNMIMAGETYPES void init_libpnmimagetypes();
   init_libpnmimagetypes();
 
   reset_frame_rate();
@@ -792,9 +807,14 @@ do_frame(Thread *current_thread) {
  */
 void PandaFramework::
 main_loop() {
+#ifdef __EMSCRIPTEN__
+  framework_cat.info() << "Starting main loop.\n";
+  emscripten_set_main_loop_arg(&em_do_frame, (void *)this, 0, true);
+#else
   Thread *current_thread = Thread::get_current_thread();
   while (do_frame(current_thread)) {
   }
+#endif
 }
 
 /**

+ 8 - 0
panda/src/glstuff/glGraphicsStateGuardian_src.I

@@ -256,8 +256,16 @@ INLINE bool CLP(GraphicsStateGuardian)::
 is_at_least_gles_version(int major_version, int minor_version) const {
 #ifndef OPENGLES
   return false;
+
 #elif defined(OPENGLES_1)
   return major_version == 1 && _gl_version_minor >= minor_version;
+
+/*#elif defined(__EMSCRIPTEN__)
+  // We're running WebGL.  WebGL 1 is based on OpenGL ES 2, and
+  // WebGL 2 is based on OpenGL ES 3.  So add one to the major version.
+  // There don't appear to be minor WebGL versions (yet).
+  return (_gl_version_major + 1 >= major_version);
+*/
 #else
   if (_gl_version_major < major_version) {
     return false;

+ 34 - 5
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -628,6 +628,7 @@ debug_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei l
 void CLP(GraphicsStateGuardian)::
 reset() {
   _last_error_check = -1.0;
+  _white_texture = 0;
 
   free_pointers();
   GraphicsStateGuardian::reset();
@@ -942,7 +943,10 @@ reset() {
 
 #elif defined(OPENGLES)
   if (gl_support_primitive_restart_index && is_at_least_gles_version(3, 0)) {
+    // In WebGL 2, primitive restart is always enabled.
+#ifndef __EMSCRIPTEN__
     glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
+#endif
     _supported_geom_rendering |= Geom::GR_strip_cut_index;
   }
 
@@ -987,6 +991,8 @@ reset() {
 #ifndef OPENGLES_1
   _glDrawRangeElements = null_glDrawRangeElements;
 
+  // Temporarily disabled in WebGL due to Firefox bug
+#ifndef __EMSCRIPTEN__
 #ifdef OPENGLES
   if (is_at_least_gles_version(3, 0)) {
     _glDrawRangeElements = (PFNGLDRAWRANGEELEMENTSPROC)
@@ -1007,6 +1013,7 @@ reset() {
       << "glDrawRangeElements advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n";
     _glDrawRangeElements = null_glDrawRangeElements;
   }
+#endif  // !__EMSCRIPTEN__
 #endif  // !OPENGLES_1
 
   _supports_3d_texture = false;
@@ -1570,7 +1577,12 @@ reset() {
     _supports_depth24 = true;
     _supports_depth32 = true;
   } else {
+#ifdef __EMSCRIPTEN__
+    if (has_extension("WEBGL_depth_texture") ||
+        has_extension("GL_ANGLE_depth_texture")) {
+#else
     if (has_extension("GL_ANGLE_depth_texture")) {
+#endif
       // This extension provides both depth textures and depth-stencil support.
       _supports_depth_texture = true;
       _supports_depth_stencil = true;
@@ -2624,6 +2636,12 @@ reset() {
     _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
       get_extension_func("glDrawBuffers");
 
+#ifdef __EMSCRIPTEN__
+  } else if (has_extension("WEBGL_draw_buffers")) {
+    _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
+      get_extension_func("glDrawBuffers");
+#endif  // EMSCRIPTEN
+
   } else if (has_extension("GL_EXT_draw_buffers")) {
     _glDrawBuffers = (PFNGLDRAWBUFFERSPROC)
       get_extension_func("glDrawBuffersEXT");
@@ -2839,7 +2857,7 @@ reset() {
 
 #elif defined(OPENGLES)
   // In OpenGL ES 2.x and above, this is supported in the core.
-  _supports_blend_equation_separate = false;
+  _supports_blend_equation_separate = true;
 
 #else
   if (is_at_least_gl_version(1, 2)) {
@@ -3300,7 +3318,7 @@ reset() {
   }
 #endif  // !OPENGLES
 
-#ifndef OPENGLES_1
+#if !defined(OPENGLES_1) && !defined(__EMSCRIPTEN__)
   _supports_get_program_binary = false;
   _program_binary_formats.clear();
 
@@ -3641,7 +3659,7 @@ reset() {
 
   report_my_gl_errors();
 
-#ifndef OPENGLES_1
+#if !defined(OPENGLES_1) && !defined(__EMSCRIPTEN__)
   if (GLCAT.is_debug()) {
     if (_supports_get_program_binary) {
       GLCAT.debug()
@@ -9633,7 +9651,7 @@ query_glsl_version() {
     if (ver.empty() ||
         sscanf(ver.c_str(), "%d.%d", &_gl_shadlang_ver_major,
                                      &_gl_shadlang_ver_minor) != 2) {
-      GLCAT.warning() << "Invalid GL_SHADING_LANGUAGE_VERSION format.\n";
+      GLCAT.warning() << "Invalid GL_SHADING_LANGUAGE_VERSION format: " << ver << "\n";
     }
   }
 #else
@@ -9644,7 +9662,11 @@ query_glsl_version() {
   if (ver.empty() ||
       sscanf(ver.c_str(), "OpenGL ES GLSL ES %d.%d", &_gl_shadlang_ver_major,
                                                      &_gl_shadlang_ver_minor) != 2) {
-    GLCAT.warning() << "Invalid GL_SHADING_LANGUAGE_VERSION format.\n";
+#ifdef __EMSCRIPTEN__  // See emscripten bug 4070
+    if (sscanf(ver.c_str(), "OpenGL ES GLSL %d.%d", &_gl_shadlang_ver_major,
+                                                    &_gl_shadlang_ver_minor) != 2)
+#endif
+    GLCAT.warning() << "Invalid GL_SHADING_LANGUAGE_VERSION format: " << ver << "\n";
   }
 #endif
 
@@ -11042,6 +11064,13 @@ get_internal_image_format(Texture *tex, bool force_sized) const {
     }
   }
 
+#if defined(__EMSCRIPTEN__) && defined(OPENGLES)
+  // WebGL 1 has no sized formats, it would seem.
+  if (!is_at_least_gles_version(3, 0)) {
+    return get_external_image_format(tex);
+  }
+#endif
+
   switch (format) {
 #ifndef OPENGLES
   case Texture::F_color_index:

+ 3 - 0
panda/src/glstuff/glGraphicsStateGuardian_src.h

@@ -770,8 +770,11 @@ protected:
   bool _supports_anisotropy;
   GLint _max_image_units;
   bool _supports_multi_bind;
+
+#if !defined(OPENGLES_1) && !defined(__EMSCRIPTEN__)
   bool _supports_get_program_binary;
   pset<GLenum> _program_binary_formats;
+#endif
 
 #ifdef OPENGLES
   bool _supports_depth24;

+ 4 - 0
panda/src/glstuff/glShaderContext_src.cxx

@@ -3296,6 +3296,7 @@ glsl_compile_and_link() {
 
   // If we requested to retrieve the shader, we should indicate that before
   // linking.
+#ifndef __EMSCRIPTEN__
   bool retrieve_binary = false;
   if (_glgsg->_supports_get_program_binary) {
     retrieve_binary = _shader->get_cache_compiled_shader();
@@ -3308,6 +3309,7 @@ glsl_compile_and_link() {
 
     _glgsg->_glProgramParameteri(_glsl_program, GL_PROGRAM_BINARY_RETRIEVABLE_HINT, GL_TRUE);
   }
+#endif  // !__EMSCRIPTEN__
 
   if (GLCAT.is_debug()) {
     GLCAT.debug()
@@ -3328,6 +3330,7 @@ glsl_compile_and_link() {
   // Report any warnings.
   glsl_report_program_errors(_glsl_program, false);
 
+#ifndef __EMSCRIPTEN__
   if (retrieve_binary) {
     GLint length = 0;
     _glgsg->_glGetProgramiv(_glsl_program, GL_PROGRAM_BINARY_LENGTH, &length);
@@ -3358,6 +3361,7 @@ glsl_compile_and_link() {
     }
 #endif  // NDEBUG
   }
+#endif  // !__EMSCRIPTEN__
 
   _glgsg->report_my_gl_errors();
   return valid;

+ 13 - 0
panda/src/gobj/texturePool.cxx

@@ -30,6 +30,10 @@
 
 #include <algorithm>
 
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
+
 using std::istream;
 using std::ostream;
 using std::string;
@@ -1070,6 +1074,15 @@ ns_release_all_textures() {
 
   // Blow away the cache of resolved relative filenames.
   _relpath_lookup.clear();
+
+#ifdef __EMSCRIPTEN__
+  // Also empty the emscripten preload cache.
+  EM_ASM({
+    if (Module["preloadedImages"]) {
+      Module["preloadedImages"] = {};
+    }
+  });
+#endif
 }
 
 /**

+ 3 - 1
panda/src/nativenet/socket_portable.h

@@ -183,7 +183,7 @@ const int LOCAL_CONNECT_BLOCKING = EINPROGRESS;
 * LINUX and FreeBSD STUFF
 ************************************************************************/
 
-#elif defined(IS_LINUX) || defined(IS_OSX) || defined(IS_FREEBSD)
+#elif defined(IS_LINUX) || defined(IS_OSX) || defined(IS_FREEBSD) || defined(__EMSCRIPTEN__)
 
 #include <sys/types.h>
 #include <sys/time.h>
@@ -246,7 +246,9 @@ inline int DO_RECV_FROM(SOCKET sck, char *data, int len, sockaddr *addr) {
 
 
 inline int init_network() {
+#ifndef __EMSCRIPTEN__
   signal(SIGPIPE, SIG_IGN); // hmm do i still need this ...
+#endif
   return ALL_OK;
 }
 

+ 2 - 0
panda/src/net/connectionManager.cxx

@@ -544,6 +544,8 @@ scan_interfaces() {
 #elif defined(__ANDROID__)
   // TODO: implementation using netlink_socket?
 
+#elif defined(__EMSCRIPTEN__)
+
 #else  // _WIN32
   struct ifaddrs *ifa;
   if (getifaddrs(&ifa) != 0) {

+ 2 - 0
panda/src/pnmimage/CMakeLists.txt

@@ -9,6 +9,7 @@ set(P3PNMIMAGE_HEADERS
   pnmPainter.h pnmPainter.I
   pnmReader.I
   pnmReader.h pnmWriter.I pnmWriter.h pnmimage_base.h
+  pnmReaderEmscripten.h
   ppmcmap.h
 )
 
@@ -24,6 +25,7 @@ set(P3PNMIMAGE_SOURCES
   pnmFileTypeRegistry.cxx pnmImage.cxx pnmImageHeader.cxx
   pnmPainter.cxx
   pnmReader.cxx pnmWriter.cxx pnmimage_base.cxx
+  pnmReaderEmscripten.cxx
   ppmcmap.cxx
 )
 

+ 1 - 0
panda/src/pnmimage/p3pnmimage_composite2.cxx

@@ -2,6 +2,7 @@
 #include "pnmImageHeader.cxx"
 #include "pnmPainter.cxx"
 #include "pnmReader.cxx"
+#include "pnmReaderEmscripten.cxx"
 #include "pnmWriter.cxx"
 #include "pnmFileTypeRegistry.cxx"
 #include "pnmimage_base.cxx" 

+ 56 - 5
panda/src/pnmimage/pnmImageHeader.cxx

@@ -20,10 +20,36 @@
 #include "virtualFileSystem.h"
 #include "zStream.h"
 
+#ifdef __EMSCRIPTEN__
+#include "subfileInfo.h"
+#include "pnmReaderEmscripten.h"
+
+#include <emscripten.h>
+#endif
+
 using std::istream;
 using std::ostream;
 using std::string;
 
+#ifdef __EMSCRIPTEN__
+/**
+ * Returns true and sets width/height if the given path is in the emscripten
+ * preload image cache.
+ */
+EM_JS(int, query_preload, (const char *path, int *width, int *height), {
+  var cache = Module["preloadedImages"];
+  if (cache) {
+    var canvas = cache[UTF8ToString(path)];
+    if (canvas) {
+      setValue(width, canvas["width"], "i32");
+      setValue(height, canvas["height"], "i32");
+      return 1;
+    }
+  }
+  return 0;
+})
+#endif
+
 /**
  * Opens up the image file and tries to read its header information to
  * determine its size, number of channels, etc.  If successful, updates the
@@ -84,11 +110,11 @@ make_reader(const Filename &filename, PNMFileType *type,
       << "Reading image from " << filename << "\n";
   }
   bool owns_file = false;
-  istream *file = nullptr;
+  istream *in = nullptr;
 
   if (filename == "-") {
     owns_file = false;
-    file = &std::cin;
+    in = &std::cin;
 
     if (pnmimage_cat.is_debug()) {
       pnmimage_cat.debug()
@@ -97,10 +123,35 @@ make_reader(const Filename &filename, PNMFileType *type,
   } else {
     VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
     owns_file = true;
-    file = vfs->open_read_file(filename, true);
+    PT(VirtualFile) file = vfs->get_file(filename, false);
+    if (file != nullptr) {
+#ifdef __EMSCRIPTEN__
+      // Is this a real file, and do we have that in the Emscripten cache?
+      SubfileInfo info;
+      if (file->get_system_info(info) && info.get_start() == 0) {
+        Filename fn = info.get_filename();
+        if (!fn.make_canonical()) {
+          fn.make_absolute();
+        }
+        int width, height;
+        if (query_preload(fn.c_str(), &width, &height)) {
+          if (pnmimage_cat.is_debug()) {
+            pnmimage_cat.debug()
+              << "(found in emscripten cache)\n";
+          }
+          return new PNMReaderEmscripten(type, fn, width, height);
+        }
+      }
+#endif
+      in = vfs->open_read_file(filename, true);
+      if (in != nullptr && in->fail()) {
+        file->close_read_file(in);
+        in = nullptr;
+      }
+    }
   }
 
-  if (file == nullptr) {
+  if (in == nullptr) {
     if (pnmimage_cat.is_debug()) {
       pnmimage_cat.debug()
         << "Unable to open file.\n";
@@ -108,7 +159,7 @@ make_reader(const Filename &filename, PNMFileType *type,
     return nullptr;
   }
 
-  return make_reader(file, owns_file, filename, string(), type,
+  return make_reader(in, owns_file, filename, string(), type,
                      report_unknown_type);
 }
 

+ 73 - 0
panda/src/pnmimage/pnmReaderEmscripten.cxx

@@ -0,0 +1,73 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pnmReaderEmscripten.cxx
+ * @author rdb
+ * @date 2021-02-05
+ */
+
+#include "pnmReaderEmscripten.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "config_pnmimage.h"
+
+#include <emscripten.h>
+
+/**
+ *
+ */
+PNMReaderEmscripten::
+PNMReaderEmscripten(PNMFileType *type, Filename fullpath, int width, int height) :
+  PNMReader(type, nullptr, false),
+  _fullpath(std::move(fullpath))
+{
+  _x_size = width;
+  _y_size = height;
+  _num_channels = 4;
+  _maxval = 255;
+}
+
+/**
+ * Reads in an entire image all at once, storing it in the pre-allocated
+ * _x_size * _y_size array and alpha pointers.  (If the image type has no
+ * alpha channel, alpha is ignored.)  Returns the number of rows correctly
+ * read.
+ *
+ * Derived classes need not override this if they instead provide
+ * supports_read_row() and read_row(), below.
+ */
+int PNMReaderEmscripten::
+read_data(xel *array, xelval *alpha) {
+  if (!is_valid()) {
+    return 0;
+  }
+
+  int width, height;
+  unsigned char *data = (unsigned char *)emscripten_get_preloaded_image_data(_fullpath.c_str(), &width, &height);
+  nassertr(data != nullptr, 0);
+  nassertr(width == _x_size, 0);
+  if (height > _x_size) {
+    height = _x_size;
+  }
+
+  size_t total_size = (size_t)width * (size_t)height;
+  for (int i = 0; i < total_size; ++i) {
+    array[i].r = data[i * 4 + 0];
+    array[i].g = data[i * 4 + 1];
+    array[i].b = data[i * 4 + 2];
+    alpha[i] = data[i * 4 + 3];
+  }
+
+  free(data);
+
+  return height;
+}
+
+
+#endif  // __EMSCRIPTEN__

+ 40 - 0
panda/src/pnmimage/pnmReaderEmscripten.h

@@ -0,0 +1,40 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pnmReaderEmscripten.h
+ * @author rdb
+ * @date 2021-02-05
+ */
+
+#ifndef PNMREADEREMSCRIPTEN_H
+#define PNMREADEREMSCRIPTEN_H
+
+#include "pandabase.h"
+
+#ifdef __EMSCRIPTEN__
+
+#include "pnmFileType.h"
+#include "pnmReader.h"
+#include "pnmWriter.h"
+
+/**
+ * For reading files using the emscripten pre-load system.
+ */
+class PNMReaderEmscripten final : public PNMReader {
+public:
+  PNMReaderEmscripten(PNMFileType *type, Filename fullpath, int width, int height);
+
+  virtual int read_data(xel *array, xelval *alpha);
+
+private:
+  Filename _fullpath;
+};
+
+#endif  // __EMSCRIPTEN__
+
+#endif

+ 1 - 1
panda/src/pnmimagetypes/config_pnmimagetypes.h

@@ -74,6 +74,6 @@ EXPCL_PANDA_PNMIMAGETYPES std::istream &operator >> (std::istream &in, IMGHeader
 extern ConfigVariableEnum<IMGHeaderType> img_header_type;
 extern ConfigVariableInt img_size;
 
-extern EXPCL_PANDA_PNMIMAGETYPES void init_libpnmimagetypes();
+extern "C" EXPCL_PANDA_PNMIMAGETYPES void init_libpnmimagetypes();
 
 #endif

+ 53 - 0
panda/src/webgldisplay/config_webgldisplay.cxx

@@ -0,0 +1,53 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file config_webgldisplay.cxx
+ * @author rdb
+ * @date 2015-04-01
+ */
+
+#include "config_webgldisplay.h"
+#include "webGLGraphicsPipe.h"
+#include "webGLGraphicsWindow.h"
+#include "webGLGraphicsStateGuardian.h"
+#include "graphicsPipeSelection.h"
+#include "dconfig.h"
+#include "pandaSystem.h"
+
+Configure(config_webgldisplay);
+NotifyCategoryDef(webgldisplay, "display");
+
+ConfigureFn(config_webgldisplay) {
+  init_libwebgldisplay();
+}
+
+/**
+ * Initializes the library.  This must be called at least once before any of
+ * the functions or classes in this library can be used.  Normally it will be
+ * called by the static initializers and need not be called explicitly, but
+ * special cases exist.
+ */
+void
+init_libwebgldisplay() {
+  static bool initialized = false;
+  if (initialized) {
+    return;
+  }
+  initialized = true;
+
+  WebGLGraphicsPipe::init_type();
+  WebGLGraphicsWindow::init_type();
+  WebGLGraphicsStateGuardian::init_type();
+
+  GraphicsPipeSelection *selection = GraphicsPipeSelection::get_global_ptr();
+  selection->add_pipe_type(WebGLGraphicsPipe::get_class_type(),
+                           WebGLGraphicsPipe::pipe_constructor);
+
+  PandaSystem *ps = PandaSystem::get_global_ptr();
+  ps->set_system_tag("WebGL", "window_system", "HTML");
+}

+ 27 - 0
panda/src/webgldisplay/config_webgldisplay.h

@@ -0,0 +1,27 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file config_webgldisplay.h
+ * @author rdb
+ * @date 2015-04-01
+ */
+
+#ifndef CONFIG_WEBGLDISPLAY_H
+#define CONFIG_WEBGLDISPLAY_H
+
+#include "pandabase.h"
+#include "notifyCategoryProxy.h"
+#include "configVariableString.h"
+#include "configVariableBool.h"
+#include "configVariableInt.h"
+
+NotifyCategoryDecl(webgldisplay,,);
+
+extern "C" void init_libwebgldisplay();
+
+#endif

+ 4 - 0
panda/src/webgldisplay/p3webgldisplay_composite1.cxx

@@ -0,0 +1,4 @@
+#include "config_webgldisplay.cxx"
+#include "webGLGraphicsPipe.cxx"
+#include "webGLGraphicsStateGuardian.cxx"
+#include "webGLGraphicsWindow.cxx"

+ 12 - 0
panda/src/webgldisplay/webGLGraphicsPipe.I

@@ -0,0 +1,12 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsPipe.I
+ * @author rdb
+ * @date 2015-04-01
+ */

+ 138 - 0
panda/src/webgldisplay/webGLGraphicsPipe.cxx

@@ -0,0 +1,138 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsPipe.cxx
+ * @author rdb
+ * @date 2015-04-01
+ */
+
+#include "webGLGraphicsPipe.h"
+#include "webGLGraphicsWindow.h"
+#include "webGLGraphicsStateGuardian.h"
+#include "config_webgldisplay.h"
+#include "frameBufferProperties.h"
+
+TypeHandle WebGLGraphicsPipe::_type_handle;
+
+/**
+ *
+ */
+WebGLGraphicsPipe::
+WebGLGraphicsPipe() {
+}
+
+/**
+ *
+ */
+WebGLGraphicsPipe::
+~WebGLGraphicsPipe() {
+}
+
+/**
+ * Returns the name of the rendering interface associated with this
+ * GraphicsPipe.  This is used to present to the user to allow him/her to
+ * choose between several possible GraphicsPipes available on a particular
+ * platform, so the name should be meaningful and unique for a given platform.
+ */
+std::string WebGLGraphicsPipe::
+get_interface_name() const {
+  return "WebGL";
+}
+
+/**
+ * This function is passed to the GraphicsPipeSelection object to allow the
+ * user to make a default webGLGraphicsPipe.
+ */
+PT(GraphicsPipe) WebGLGraphicsPipe::
+pipe_constructor() {
+  return new WebGLGraphicsPipe;
+}
+
+/**
+ * Returns an indication of the thread in which this GraphicsPipe requires its
+ * window processing to be performed: typically either the app thread (e.g.
+ * X) or the draw thread (Windows).
+ */
+GraphicsPipe::PreferredWindowThread WebGLGraphicsPipe::
+get_preferred_window_thread() const {
+  // JavaScript has no threads, so does it matter?
+  return PWT_draw;
+}
+
+/**
+ * Creates a new window on the pipe, if possible.
+ */
+PT(GraphicsOutput) WebGLGraphicsPipe::
+make_output(const std::string &name,
+            const FrameBufferProperties &fb_prop,
+            const WindowProperties &win_prop,
+            int flags,
+            GraphicsEngine *engine,
+            GraphicsStateGuardian *gsg,
+            GraphicsOutput *host,
+            int retry,
+            bool &precertify) {
+
+  if (!_is_valid) {
+    return NULL;
+  }
+
+  WebGLGraphicsStateGuardian *web_gl = 0;
+  if (gsg != NULL) {
+    DCAST_INTO_R(web_gl, gsg, NULL);
+  }
+
+  // First thing to try: a WebGLGraphicsWindow
+
+  if (retry == 0) {
+    if (((flags&BF_require_parasite)!=0)||
+        ((flags&BF_refuse_window)!=0)||
+        ((flags&BF_resizeable)!=0)||
+        ((flags&BF_size_track_host)!=0)||
+        ((flags&BF_rtt_cumulative)!=0)||
+        ((flags&BF_can_bind_color)!=0)||
+        ((flags&BF_can_bind_every)!=0)) {
+      return NULL;
+    }
+    return new WebGLGraphicsWindow(engine, this, name, fb_prop, win_prop,
+                                   flags, gsg, host);
+  }
+
+  // Second thing to try: a GLES2GraphicsBuffer
+  if (retry == 1) {
+    if ((host==0)||
+        //(!gl_support_fbo)||
+        ((flags&BF_require_parasite)!=0)||
+        ((flags&BF_require_window)!=0)) {
+      return NULL;
+    }
+    // Early failure - if we are sure that this buffer WONT meet specs, we can
+    // bail out early.
+    if ((flags & BF_fb_props_optional)==0) {
+      if ((fb_prop.get_indexed_color() > 0)||
+          (fb_prop.get_back_buffers() > 0)||
+          (fb_prop.get_accum_bits() > 0)||
+          (fb_prop.get_multisamples() > 0)) {
+        return NULL;
+      }
+    }
+    // Early success - if we are sure that this buffer WILL meet specs, we can
+    // precertify it.
+    if ((web_gl != 0) &&
+        (web_gl->is_valid()) &&
+        (!web_gl->needs_reset()) &&
+        (fb_prop.is_basic())) {
+      precertify = true;
+    }
+    return new GLES2GraphicsBuffer(engine, this, name, fb_prop, win_prop,
+                                  flags, gsg, host);
+  }
+
+  // Nothing else left to try.
+  return NULL;
+}

+ 71 - 0
panda/src/webgldisplay/webGLGraphicsPipe.h

@@ -0,0 +1,71 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsPipe.h
+ * @author rdb
+ * @date 2015-04-01
+ */
+
+#ifndef WEBGLGRAPHICSPIPE_H
+#define WEBGLGRAPHICSPIPE_H
+
+#include "pandabase.h"
+#include "graphicsWindow.h"
+#include "graphicsPipe.h"
+
+class FrameBufferProperties;
+
+class WebGLGraphicsWindow;
+
+/**
+ * This graphics pipe represents the interface for creating OpenGL ES graphics
+ * windows on an X-based (e.g.  Unix) client.
+ */
+class WebGLGraphicsPipe : public GraphicsPipe {
+public:
+  WebGLGraphicsPipe();
+  virtual ~WebGLGraphicsPipe();
+
+  virtual std::string get_interface_name() const;
+  static PT(GraphicsPipe) pipe_constructor();
+
+public:
+  virtual PreferredWindowThread get_preferred_window_thread() const;
+
+protected:
+  virtual PT(GraphicsOutput) make_output(const std::string &name,
+                                         const FrameBufferProperties &fb_prop,
+                                         const WindowProperties &win_prop,
+                                         int flags,
+                                         GraphicsEngine *engine,
+                                         GraphicsStateGuardian *gsg,
+                                         GraphicsOutput *host,
+                                         int retry,
+                                         bool &precertify);
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    GraphicsPipe::init_type();
+    register_type(_type_handle, "WebGLGraphicsPipe",
+                  GraphicsPipe::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#include "webGLGraphicsPipe.I"
+
+#endif

+ 12 - 0
panda/src/webgldisplay/webGLGraphicsStateGuardian.I

@@ -0,0 +1,12 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsStateGuardian.I
+ * @author rdb
+ * @date 2015-04-01
+ */

+ 174 - 0
panda/src/webgldisplay/webGLGraphicsStateGuardian.cxx

@@ -0,0 +1,174 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsStateGuardian.cxx
+ * @author rdb
+ * @date 2015-04-01
+ */
+
+#include "webGLGraphicsStateGuardian.h"
+#include "config_webgldisplay.h"
+
+extern "C" {
+  extern void* emscripten_GetProcAddress(const char *x);
+}
+
+TypeHandle WebGLGraphicsStateGuardian::_type_handle;
+
+/**
+ *
+ */
+WebGLGraphicsStateGuardian::
+WebGLGraphicsStateGuardian(GraphicsEngine *engine, GraphicsPipe *pipe) :
+  GLES2GraphicsStateGuardian(engine, pipe)
+{
+  _context = 0;
+  _have_context = false;
+}
+
+/**
+ *
+ */
+WebGLGraphicsStateGuardian::
+~WebGLGraphicsStateGuardian() {
+  if (_context != 0) {
+    const char *target = "#canvas";
+    emscripten_set_webglcontextlost_callback(target, NULL, false, NULL);
+    emscripten_set_webglcontextrestored_callback(target, NULL, false, NULL);
+
+    if (emscripten_webgl_destroy_context(_context) != EMSCRIPTEN_RESULT_SUCCESS) {
+      webgldisplay_cat.error() << "Failed to destroy WebGL context!\n";
+    }
+    _context = 0;
+  }
+}
+
+/**
+ * Selects a visual or fbconfig for all the windows and buffers that use this
+ * gsg.  Also creates the GL context and obtains the visual.
+ */
+void WebGLGraphicsStateGuardian::
+choose_pixel_format(const FrameBufferProperties &properties,
+                    const char *target) {
+
+  nassertv(_context == 0);
+
+  EmscriptenWebGLContextAttributes attribs;
+  emscripten_webgl_init_context_attributes(&attribs);
+
+  attribs.alpha = (properties.get_alpha_bits() > 0);
+  attribs.depth = (properties.get_depth_bits() > 0);
+  attribs.stencil = (properties.get_stencil_bits() > 0);
+  attribs.majorVersion = 2;
+  attribs.minorVersion = 0;
+  attribs.enableExtensionsByDefault = false;
+
+  EMSCRIPTEN_WEBGL_CONTEXT_HANDLE result;
+  result = emscripten_webgl_create_context(target, &attribs);
+
+  if (result <= 0) {
+    // Fall back to WebGL 1.
+    attribs.majorVersion = 1;
+    result = emscripten_webgl_create_context(target, &attribs);
+  }
+
+  if (result > 0) {
+    _context = result;
+    _have_context = true;
+
+    // We may lose the WebGL context at any time, at which time we have to be
+    // prepared to drop all resources.
+    emscripten_set_webglcontextlost_callback(target, (void *)this, false, &on_context_event);
+    emscripten_set_webglcontextrestored_callback(target, (void *)this, false, &on_context_event);
+  } else {
+    webgldisplay_cat.error() << "Context creation failed.\n";
+  }
+}
+
+/**
+ * Resets all internal state as if the gsg were newly created.
+ */
+void WebGLGraphicsStateGuardian::
+reset() {
+  GLES2GraphicsStateGuardian::reset();
+}
+
+/**
+ * WebGL may take the context away from us at any time.  We have to be ready
+ * for that.
+ */
+EM_BOOL WebGLGraphicsStateGuardian::
+on_context_event(int type, const void *, void *user_data) {
+  WebGLGraphicsStateGuardian *gsg = (WebGLGraphicsStateGuardian *)user_data;
+  nassertr(gsg != NULL, false);
+
+  if (type == EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST) {
+    webgldisplay_cat.info() << "WebGL context lost!\n";
+    gsg->context_lost();
+    return true;
+
+  } else if (type == EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED) {
+    webgldisplay_cat.info() << "WebGL context restored!\n";
+    gsg->reset();
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ * Called to indicate that we lost the WebGL context, and any resources we
+ * prepared previously are gone.
+ */
+void WebGLGraphicsStateGuardian::
+context_lost() {
+  release_all();
+
+  // Assign a new PreparedGraphicsObjects to prevent the GSG from to trying to
+  // neatly clean up the old resources.
+  _prepared_objects = new PreparedGraphicsObjects;
+}
+
+/**
+ * This may be redefined by a derived class (e.g.  glx or wgl) to get whatever
+ * further extensions strings may be appropriate to that interface, in
+ * addition to the GL extension strings return by glGetString().
+ */
+void WebGLGraphicsStateGuardian::
+get_extra_extensions() {
+}
+
+/**
+ * Returns true if the indicated extension is reported by the GL system, false
+ * otherwise.  The extension name is case-sensitive.
+ */
+bool WebGLGraphicsStateGuardian::
+has_extension(const std::string &extension) const {
+  nassertr(_context != 0, false);
+
+  // If the GSG asks for it, that is probably a good reason to activate the
+  // extension.
+  EM_BOOL result = emscripten_webgl_enable_extension(_context, extension.c_str());
+#ifndef NDEBUG
+  if (result) {
+    webgldisplay_cat.info() << "Activated extension: " << extension << "\n";
+  }
+#endif
+  return result;
+}
+
+/**
+ * Returns the pointer to the GL extension function with the indicated name.
+ * It is the responsibility of the caller to ensure that the required
+ * extension is defined in the OpenGL runtime prior to calling this; it is an
+ * error to call this for a function that is not defined.
+ */
+void *WebGLGraphicsStateGuardian::
+do_get_extension_func(const char *name) {
+  return emscripten_GetProcAddress(name);
+}

+ 72 - 0
panda/src/webgldisplay/webGLGraphicsStateGuardian.h

@@ -0,0 +1,72 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsStateGuardian.h
+ * @author rdb
+ * @date 2015-04-01
+ */
+
+#ifndef WEBGLGRAPHICSSTATEGUARDIAN_H
+#define WEBGLGRAPHICSSTATEGUARDIAN_H
+
+#include "pandabase.h"
+#include "webGLGraphicsPipe.h"
+#include "gles2gsg.h"
+
+#include <emscripten/html5.h>
+
+/**
+ * A specialization on GLES2GraphicsStateGuardian to add emscripten-specific
+ * context set-up.
+ */
+class WebGLGraphicsStateGuardian : public GLES2GraphicsStateGuardian {
+public:
+  WebGLGraphicsStateGuardian(GraphicsEngine *engine, GraphicsPipe *pipe);
+  virtual ~WebGLGraphicsStateGuardian();
+
+  void choose_pixel_format(const FrameBufferProperties &properties,
+                           const char *target);
+
+  static EM_BOOL on_context_event(int type, const void *, void *user_data);
+
+  virtual void reset();
+
+private:
+  EMSCRIPTEN_WEBGL_CONTEXT_HANDLE _context;
+  bool _have_context;
+
+protected:
+  void context_lost();
+
+  virtual void get_extra_extensions();
+  virtual bool has_extension(const std::string &extension) const;
+  virtual void *do_get_extension_func(const char *name);
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    GLES2GraphicsStateGuardian::init_type();
+    register_type(_type_handle, "WebGLGraphicsStateGuardian",
+                  GLES2GraphicsStateGuardian::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+
+  friend class WebGLGraphicsWindow;
+};
+
+#include "webGLGraphicsStateGuardian.I"
+
+#endif

+ 12 - 0
panda/src/webgldisplay/webGLGraphicsWindow.I

@@ -0,0 +1,12 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsWindow.I
+ * @author rdb
+ * @date 2015-03-31
+ */

+ 971 - 0
panda/src/webgldisplay/webGLGraphicsWindow.cxx

@@ -0,0 +1,971 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsWindow.cxx
+ * @author rdb
+ * @date 2015-03-31
+ */
+
+#include "webGLGraphicsWindow.h"
+#include "webGLGraphicsStateGuardian.h"
+#include "config_webgldisplay.h"
+#include "mouseButton.h"
+#include "keyboardButton.h"
+#include "throw_event.h"
+#include "pointerData.h"
+#include <emscripten.h>
+
+TypeHandle WebGLGraphicsWindow::_type_handle;
+
+/**
+ *
+ */
+WebGLGraphicsWindow::
+WebGLGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
+                    const std::string &name,
+                    const FrameBufferProperties &fb_prop,
+                    const WindowProperties &win_prop,
+                    int flags,
+                    GraphicsStateGuardian *gsg,
+                    GraphicsOutput *host) :
+  GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host)
+{
+  PT(GraphicsWindowInputDevice) device =
+    GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse");
+  add_input_device(device);
+  _input = device.p();
+}
+
+/**
+ *
+ */
+WebGLGraphicsWindow::
+~WebGLGraphicsWindow() {
+}
+
+/**
+ * Forces the pointer to the indicated position within the window, if
+ * possible.
+ *
+ * Returns true if successful, false on failure.  This may fail if the mouse
+ * is not currently within the window, or if the API doesn't support this
+ * operation.
+ */
+bool WebGLGraphicsWindow::
+move_pointer(int device, int x, int y) {
+  if (device == 0 && _properties.get_mouse_mode() == WindowProperties::M_relative) {
+    // The pointer position is meaningless in relative mode, so we silently
+    // pretend that this worked.
+    _input->set_pointer_in_window(x, y);
+    return true;
+  }
+  return false;
+}
+
+/**
+ * This function will be called within the draw thread before beginning
+ * rendering for a given frame.  It should do whatever setup is required, and
+ * return true if the frame should be rendered, or false if it should be
+ * skipped.
+ */
+bool WebGLGraphicsWindow::
+begin_frame(FrameMode mode, Thread *current_thread) {
+  PStatTimer timer(_make_current_pcollector, current_thread);
+
+  begin_frame_spam(mode);
+  if (_gsg == nullptr) {
+    return false;
+  }
+
+  WebGLGraphicsStateGuardian *webgl_gsg;
+  DCAST_INTO_R(webgl_gsg, _gsg, false);
+
+  if (emscripten_is_webgl_context_lost(webgl_gsg->_context)) {
+    // The context was lost, and any GL calls we make will fail.
+    return false;
+  }
+
+  if (emscripten_webgl_make_context_current(webgl_gsg->_context) != EMSCRIPTEN_RESULT_SUCCESS) {
+    webgldisplay_cat.error()
+      << "Failed to make context current.\n";
+    return false;
+  }
+
+  if (mode == FM_render) {
+    // begin_render_texture();
+    clear_cube_map_selection();
+  }
+
+  _gsg->set_current_properties(&get_fb_properties());
+  return _gsg->begin_frame(current_thread);
+}
+
+/**
+ * This function will be called within the draw thread after rendering is
+ * completed for a given frame.  It should do whatever finalization is
+ * required.
+ */
+void WebGLGraphicsWindow::
+end_frame(FrameMode mode, Thread *current_thread) {
+  end_frame_spam(mode);
+  nassertv(_gsg != nullptr);
+
+  if (mode == FM_render) {
+    // end_render_texture();
+    copy_to_textures();
+  }
+
+  _gsg->end_frame(current_thread);
+
+  if (mode == FM_render) {
+    trigger_flip();
+    clear_cube_map_selection();
+  }
+}
+
+/**
+ * This function will be called within the draw thread after begin_flip() has
+ * been called on all windows, to finish the exchange of the front and back
+ * buffers.
+ *
+ * This should cause the window to wait for the flip, if necessary.
+ */
+void WebGLGraphicsWindow::
+end_flip() {
+  GraphicsWindow::end_flip();
+}
+
+/**
+ * Do whatever processing is necessary to ensure that the window responds to
+ * user events.  Also, honor any requests recently made via
+ * request_properties()
+ *
+ * This function is called only within the window thread.
+ */
+void WebGLGraphicsWindow::
+process_events() {
+  GraphicsWindow::process_events();
+}
+
+/**
+ * Applies the requested set of properties to the window, if possible, for
+ * instance to request a change in size or minimization status.
+ *
+ * The window properties are applied immediately, rather than waiting until
+ * the next frame.  This implies that this method may *only* be called from
+ * within the window thread.
+ *
+ * The return value is true if the properties are set, false if they are
+ * ignored.  This is mainly useful for derived classes to implement extensions
+ * to this function.
+ */
+void WebGLGraphicsWindow::
+set_properties_now(WindowProperties &properties) {
+  GraphicsWindow::set_properties_now(properties);
+
+  const char *target = "#canvas";
+
+  if (properties.has_size()) {
+    emscripten_set_canvas_element_size(target, properties.get_x_size(), properties.get_y_size());
+    _properties.set_size(properties.get_size());
+    properties.clear_size();
+    set_size_and_recalc(_properties.get_x_size(), _properties.get_y_size());
+    throw_event(get_window_event(), this);
+  }
+
+  if (properties.has_fullscreen() &&
+      properties.get_fullscreen() != _properties.get_fullscreen()) {
+
+    if (properties.get_fullscreen()) {
+      EMSCRIPTEN_RESULT result = emscripten_request_fullscreen(target, true);
+
+      if (result == EMSCRIPTEN_RESULT_SUCCESS) {
+        _properties.set_fullscreen(true);
+        properties.clear_fullscreen();
+
+      } else if (result == EMSCRIPTEN_RESULT_DEFERRED) {
+        // We can't switch to fullscreen just yet - this action is deferred
+        // until we're in an event handler.  We can't know for sure yet that
+        // fullscreen will be supported, but we shouldn't report failure.
+        properties.clear_fullscreen();
+      }
+    } else {
+      if (emscripten_exit_fullscreen() == EMSCRIPTEN_RESULT_SUCCESS) {
+        _properties.set_fullscreen(false);
+        properties.clear_fullscreen();
+      }
+    }
+  }
+
+  if (properties.has_mouse_mode() &&
+      properties.get_mouse_mode() != _properties.get_mouse_mode()) {
+
+    if (properties.get_mouse_mode() == WindowProperties::M_relative) {
+      EMSCRIPTEN_RESULT result = emscripten_request_pointerlock(target, true);
+
+      if (result == EMSCRIPTEN_RESULT_SUCCESS) {
+        // Great, we're in pointerlock mode.
+        _properties.set_mouse_mode(WindowProperties::M_absolute);
+        _properties.set_cursor_hidden(true);
+        properties.clear_mouse_mode();
+
+      } else if (result == EMSCRIPTEN_RESULT_DEFERRED) {
+        // We can't switch to pointerlock just yet - this action is deferred
+        // until we're in an event handler.  We can't know for sure yet that
+        // pointerlock will be supported, but we shouldn't report failure.
+        properties.clear_mouse_mode();
+        if (properties.has_cursor_hidden() && properties.get_cursor_hidden()) {
+          properties.clear_cursor_hidden();
+        }
+      }
+    } else {
+      if (emscripten_exit_pointerlock() == EMSCRIPTEN_RESULT_SUCCESS) {
+        _properties.set_mouse_mode(WindowProperties::M_absolute);
+        properties.clear_mouse_mode();
+        properties.clear_cursor_hidden();
+      }
+    }
+  }
+
+  if (properties.has_cursor_hidden() &&
+      properties.get_cursor_hidden() == (_properties.get_mouse_mode() == WindowProperties::M_relative)) {
+    // A hidden cursor comes for free with pointerlock.  Without pointerlock,
+    // though, we can't hide the cursor.
+    properties.clear_cursor_hidden();
+  }
+}
+
+/**
+ * Closes the window right now.  Called from the window thread.
+ */
+void WebGLGraphicsWindow::
+close_window() {
+  if (_gsg != nullptr) {
+    emscripten_webgl_make_context_current(0);
+    _gsg.clear();
+  }
+
+  // Clear the assigned callbacks.
+  const char *target = "#canvas";
+  emscripten_set_fullscreenchange_callback(target, nullptr, false, nullptr);
+  emscripten_set_pointerlockchange_callback(target, nullptr, false, nullptr);
+  emscripten_set_visibilitychange_callback(nullptr, false, nullptr);
+
+  emscripten_set_focus_callback(target, nullptr, false, nullptr);
+  emscripten_set_blur_callback(target, nullptr, false, nullptr);
+
+  emscripten_set_keypress_callback(target, nullptr, false, nullptr);
+  emscripten_set_keydown_callback(target, nullptr, false, nullptr);
+  emscripten_set_keyup_callback(target, nullptr, false, nullptr);
+
+  //emscripten_set_click_callback(target, nullptr, false, nullptr);
+  emscripten_set_mousedown_callback(target, nullptr, false, nullptr);
+  emscripten_set_mouseup_callback(target, nullptr, false, nullptr);
+  emscripten_set_mousemove_callback(target, nullptr, false, nullptr);
+  emscripten_set_mouseenter_callback(target, nullptr, false, nullptr);
+  emscripten_set_mouseleave_callback(target, nullptr, false, nullptr);
+
+  emscripten_set_wheel_callback(target, nullptr, false, nullptr);
+
+  GraphicsWindow::close_window();
+}
+
+/**
+ * Opens the window right now.  Called from the window thread.  Returns true
+ * if the window is successfully opened, or false if there was a problem.
+ */
+bool WebGLGraphicsWindow::
+open_window() {
+  //WebGLGraphicsPipe *webgl_pipe;
+  //DCAST_INTO_R(webgl_pipe, _pipe, false);
+
+  const char *target = "#canvas";
+
+  // GSG Creation/Initialization
+  WebGLGraphicsStateGuardian *webgl_gsg;
+  if (_gsg == nullptr) {
+    // There is no old gsg.  Create a new one.
+    webgl_gsg = new WebGLGraphicsStateGuardian(_engine, _pipe);
+    webgl_gsg->choose_pixel_format(_fb_properties, target);
+    _gsg = webgl_gsg;
+  } else {
+    // If the old gsg has the wrong pixel format, create a new one.
+    DCAST_INTO_R(webgl_gsg, _gsg, false);
+    //if (!webgl_gsg->_fb_properties.subsumes(_fb_properties)) {
+    //  webgl_gsg = new WebGLGraphicsStateGuardian(_engine, _pipe);
+    //  webgl_gsg->choose_pixel_format(_fb_properties, target);
+    //  _gsg = webgl_gsg;
+    //}
+  }
+
+  if (!webgl_gsg->_have_context) {
+    return false;
+  }
+
+  if (_properties.has_size() && _properties.get_size() != LVecBase2i(1, 1)) {
+    emscripten_set_canvas_element_size(target, _properties.get_x_size(), _properties.get_y_size());
+  } else {
+    int width, height;
+    emscripten_get_canvas_element_size(target, &width, &height);
+    _properties.set_size(width, height);
+
+    EmscriptenFullscreenChangeEvent event;
+    emscripten_get_fullscreen_status(&event);
+    _properties.set_fullscreen(event.isFullscreen);
+  }
+
+  _properties.set_undecorated(true);
+
+  if (emscripten_webgl_make_context_current(webgl_gsg->_context) != EMSCRIPTEN_RESULT_SUCCESS) {
+    webgldisplay_cat.error()
+      << "Failed to make context current.\n";
+    return false;
+  }
+
+  webgl_gsg->reset();
+
+  _fb_properties.clear();
+  _fb_properties.set_rgb_color(true);
+  _fb_properties.set_force_hardware(true);
+  _fb_properties.set_back_buffers(1);
+
+  GLint red_bits, green_bits, blue_bits, alpha_bits, depth_bits, stencil_bits;
+
+  glGetIntegerv(GL_RED_BITS, &red_bits);
+  glGetIntegerv(GL_GREEN_BITS, &green_bits);
+  glGetIntegerv(GL_BLUE_BITS, &blue_bits);
+  glGetIntegerv(GL_ALPHA_BITS, &alpha_bits);
+  glGetIntegerv(GL_DEPTH_BITS, &depth_bits);
+  glGetIntegerv(GL_STENCIL_BITS, &stencil_bits);
+
+  _fb_properties.set_rgba_bits(red_bits, green_bits, blue_bits, alpha_bits);
+  _fb_properties.set_depth_bits(depth_bits);
+  _fb_properties.set_stencil_bits(stencil_bits);
+
+  // Set callbacks.
+  emscripten_set_fullscreenchange_callback(target, (void *)this, false, &on_fullscreen_event);
+  emscripten_set_pointerlockchange_callback(target, (void *)this, false, &on_pointerlock_event);
+  emscripten_set_visibilitychange_callback((void *)this, false, &on_visibility_event);
+
+  emscripten_set_focus_callback(target, (void *)this, false, &on_focus_event);
+  emscripten_set_blur_callback(target, (void *)this, false, &on_focus_event);
+
+  void *user_data = _input;
+  emscripten_set_keypress_callback(target, user_data, false, &on_keyboard_event);
+  emscripten_set_keydown_callback(target, user_data, false, &on_keyboard_event);
+  emscripten_set_keyup_callback(target, user_data, false, &on_keyboard_event);
+
+  //emscripten_set_click_callback(target, user_data, false, &on_mouse_event);
+  emscripten_set_mousedown_callback(target, user_data, false, &on_mouse_event);
+  emscripten_set_mouseup_callback(target, user_data, false, &on_mouse_event);
+  emscripten_set_mousemove_callback(target, user_data, false, &on_mouse_event);
+  emscripten_set_mouseenter_callback(target, user_data, false, &on_mouse_event);
+  emscripten_set_mouseleave_callback(target, user_data, false, &on_mouse_event);
+
+  emscripten_set_wheel_callback(target, user_data, false, &on_wheel_event);
+
+  return true;
+}
+
+/**
+ *
+ */
+EM_BOOL WebGLGraphicsWindow::
+on_fullscreen_event(int type, const EmscriptenFullscreenChangeEvent *event, void *user_data) {
+  WebGLGraphicsWindow *window = (WebGLGraphicsWindow *)user_data;
+  nassertr(window != nullptr, false);
+
+  if (type == EMSCRIPTEN_EVENT_FULLSCREENCHANGE) {
+    WindowProperties props;
+    props.set_fullscreen(event->isFullscreen);
+    window->system_changed_properties(props);
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ *
+ */
+EM_BOOL WebGLGraphicsWindow::
+on_pointerlock_event(int type, const EmscriptenPointerlockChangeEvent *event, void *user_data) {
+  WebGLGraphicsWindow *window = (WebGLGraphicsWindow *)user_data;
+  nassertr(window != nullptr, false);
+
+  if (type == EMSCRIPTEN_EVENT_POINTERLOCKCHANGE) {
+    WindowProperties props;
+    if (event->isActive) {
+      std::cout << "pointerlock engaged\n";
+      props.set_mouse_mode(WindowProperties::M_relative);
+      props.set_cursor_hidden(true);
+    } else {
+      std::cout << "pointerlock disabled\n";
+      props.set_mouse_mode(WindowProperties::M_absolute);
+      props.set_cursor_hidden(false);
+    }
+    window->system_changed_properties(props);
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ *
+ */
+EM_BOOL WebGLGraphicsWindow::
+on_visibility_event(int type, const EmscriptenVisibilityChangeEvent *event, void *user_data) {
+  WebGLGraphicsWindow *window = (WebGLGraphicsWindow *)user_data;
+  nassertr(window != nullptr, false);
+
+  if (type == EMSCRIPTEN_EVENT_VISIBILITYCHANGE) {
+    WindowProperties props;
+    props.set_minimized(event->hidden != 0);
+    window->system_changed_properties(props);
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ *
+ */
+EM_BOOL WebGLGraphicsWindow::
+on_focus_event(int type, const EmscriptenFocusEvent *event, void *user_data) {
+  WebGLGraphicsWindow *window = (WebGLGraphicsWindow *)user_data;
+  nassertr(window != nullptr, false);
+
+  if (type == EMSCRIPTEN_EVENT_FOCUS) {
+    WindowProperties props;
+    props.set_foreground(true);
+    window->system_changed_properties(props);
+    return true;
+  } else if (type == EMSCRIPTEN_EVENT_BLUR) {
+    WindowProperties props;
+    props.set_foreground(false);
+    window->system_changed_properties(props);
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ * Handles HTML5 keypress, keydown and keyup events.
+ */
+EM_BOOL WebGLGraphicsWindow::
+on_keyboard_event(int type, const EmscriptenKeyboardEvent *event, void *user_data) {
+  GraphicsWindowInputDevice *device;
+  device = (GraphicsWindowInputDevice *)user_data;
+  nassertr(device != nullptr, false);
+
+  if (type == EMSCRIPTEN_EVENT_KEYPRESS) {
+    // We have to use String.fromCharCode to turn this into a text character.
+    // When called from a key event, it does some special magic to ensure that
+    // it does the right thing.  We grab the first unicode code point.
+    // Unfortunately, this doesn't seem to handle dead keys on Firefox.
+    int keycode = 0;
+    EM_ASM_({
+      stringToUTF32(String.fromCharCode($0), $1, 4);
+    }, event->charCode, &keycode);
+
+    if (keycode != 0) {
+      device->keystroke(keycode);
+      return true;
+    }
+
+  } else if (type == EMSCRIPTEN_EVENT_KEYDOWN ||
+             type == EMSCRIPTEN_EVENT_KEYUP) {
+
+    ButtonHandle handle = map_key(event->which);
+
+    // Send a raw event too, if the browser supports providing it.
+    ButtonHandle raw_handle;
+    if (event->code[0]) {
+      raw_handle = map_raw_key(event->code);
+    } else {
+      // This browser doesn't send raw events.  Let's just pretend the user is
+      // using QWERTY.  Better than nothing?
+      raw_handle = handle;
+    }
+
+    if (raw_handle != ButtonHandle::none()) {
+      if (type == EMSCRIPTEN_EVENT_KEYUP) {
+        device->raw_button_up(raw_handle);
+      } else if (!event->repeat) {
+        device->raw_button_down(raw_handle);
+      }
+    }
+
+    // Send a regular event for the 'virtual' key mapping.
+    if (handle != ButtonHandle::none()) {
+      if (type == EMSCRIPTEN_EVENT_KEYUP) {
+        //webgldisplay_cat.info() << "button up " << handle << "\n";
+        device->button_up(handle);
+      } else if (event->repeat) {
+        device->button_resume_down(handle);
+      } else {
+        //webgldisplay_cat.info() << "button down " << handle << "\n";
+        device->button_down(handle);
+      }
+
+      // If we preventDefault a keydown event, its keypress event won't fire,
+      // which would prevent text input from working.  However, we must still
+      // prevent the default action from working in backspace and tab events.
+      if (type == EMSCRIPTEN_EVENT_KEYDOWN &&
+          event->keyCode != 8 && event->keyCode != 9) {
+        return false;
+      } else {
+        return true;
+      }
+    } else if (event->which != 0) {
+      webgldisplay_cat.info()
+        << "unhandled event code " << event->which << "\n";
+    }
+  }
+
+  return false;
+}
+
+/**
+ * Handles mousedown, mouseup, mousemove, mouseenter and mouseleave events.
+ */
+EM_BOOL WebGLGraphicsWindow::
+on_mouse_event(int type, const EmscriptenMouseEvent *event, void *user_data) {
+  GraphicsWindowInputDevice *device;
+  device = (GraphicsWindowInputDevice *)user_data;
+  nassertr(device != nullptr, false);
+
+  const char *target = "#canvas";
+
+  switch (type) {
+  case EMSCRIPTEN_EVENT_MOUSEDOWN:
+    // Don't register out-of-bounds mouse downs.
+    if (event->targetX >= 0 && event->targetY >= 0) {
+      int w, h;
+      emscripten_get_canvas_element_size(target, &w, &h);
+      if (event->targetX < w && event->targetY < h) {
+        device->button_down(MouseButton::button(event->button));
+        return true;
+      }
+    }
+    return false;
+
+  case EMSCRIPTEN_EVENT_MOUSEUP:
+    device->button_up(MouseButton::button(event->button));
+    return true;
+
+  case EMSCRIPTEN_EVENT_MOUSEMOVE:
+    {
+      EmscriptenPointerlockChangeEvent ev;
+      emscripten_get_pointerlock_status(&ev);
+
+      if (ev.isActive) {
+        PointerData md = device->get_pointer();
+        device->set_pointer_in_window(md.get_x() + event->movementX,
+                                      md.get_y() + event->movementY);
+      } else {
+        device->set_pointer_in_window(event->targetX, event->targetY);
+      }
+    }
+    return true;
+
+  case EMSCRIPTEN_EVENT_MOUSEENTER:
+    break;
+
+  case EMSCRIPTEN_EVENT_MOUSELEAVE:
+    device->set_pointer_out_of_window();
+    return true;
+
+  default:
+    break;
+  }
+
+  return false;
+}
+
+/**
+ *
+ */
+EM_BOOL WebGLGraphicsWindow::
+on_wheel_event(int type, const EmscriptenWheelEvent *event, void *user_data) {
+  GraphicsWindowInputDevice *device;
+  device = (GraphicsWindowInputDevice *)user_data;
+  nassertr(device != nullptr, false);
+
+  if (type == EMSCRIPTEN_EVENT_WHEEL) {
+    if (event->deltaY < 0) {
+      device->button_down(MouseButton::wheel_up());
+      device->button_up(MouseButton::wheel_up());
+    }
+    if (event->deltaY > 0) {
+      device->button_down(MouseButton::wheel_down());
+      device->button_up(MouseButton::wheel_down());
+    }
+    return true;
+  }
+
+  return false;
+}
+
+
+/**
+ * Maps a JavaScript event keycode to a ButtonHandle.
+ */
+ButtonHandle WebGLGraphicsWindow::
+map_key(int which) {
+  switch (which) {
+  case 8:
+    return KeyboardButton::backspace();
+  case 9:
+    return KeyboardButton::tab();
+  case 13:
+    return KeyboardButton::enter();
+  case 16:
+    return KeyboardButton::shift();
+  case 17:
+    return KeyboardButton::control();
+  case 18:
+    return KeyboardButton::alt();
+  case 19:
+    return KeyboardButton::pause();
+  case 20:
+    return KeyboardButton::caps_lock();
+  case 27:
+    return KeyboardButton::escape();
+  case 32:
+    return KeyboardButton::space();
+  case 33:
+    return KeyboardButton::page_up();
+  case 34:
+    return KeyboardButton::page_down();
+  case 35:
+    return KeyboardButton::end();
+  case 36:
+    return KeyboardButton::home();
+  case 37:
+    return KeyboardButton::left();
+  case 38:
+    return KeyboardButton::up();
+  case 39:
+    return KeyboardButton::right();
+  case 40:
+    return KeyboardButton::down();
+  case 42:
+    return KeyboardButton::print_screen();
+  case 45:
+    return KeyboardButton::insert();
+  case 46:
+    return KeyboardButton::del();
+  case 48:
+  case 49:
+  case 50:
+  case 51:
+  case 52:
+  case 53:
+  case 54:
+  case 55:
+  case 56:
+  case 57:
+    return KeyboardButton::ascii_key('0' + (which - 48));
+  case 59:
+    return KeyboardButton::ascii_key(';');
+  case 61:
+    return KeyboardButton::ascii_key('=');
+  case 65:
+  case 66:
+  case 67:
+  case 68:
+  case 69:
+  case 70:
+  case 71:
+  case 72:
+  case 73:
+  case 74:
+  case 75:
+  case 76:
+  case 77:
+  case 78:
+  case 79:
+  case 80:
+  case 81:
+  case 82:
+  case 83:
+  case 84:
+  case 85:
+  case 86:
+  case 87:
+  case 88:
+  case 89:
+  case 90:
+    return KeyboardButton::ascii_key('a' + (which - 65));
+  case 91:
+    return KeyboardButton::lmeta();
+  case 92:
+    return KeyboardButton::rmeta();
+  case 93:
+    return KeyboardButton::menu();
+  case 96:
+  case 97:
+  case 98:
+  case 99:
+  case 100:
+  case 101:
+  case 102:
+  case 103:
+  case 104:
+  case 105:
+    return KeyboardButton::ascii_key('0' + (which - 96));
+  case 106:
+    return KeyboardButton::ascii_key('*');
+  case 107:
+    return KeyboardButton::ascii_key('+');
+  case 109:
+    return KeyboardButton::ascii_key('-');
+  case 110:
+    return KeyboardButton::ascii_key('.');
+  case 111:
+    return KeyboardButton::ascii_key('/');
+  case 112:
+    return KeyboardButton::f1();
+  case 113:
+    return KeyboardButton::f2();
+  case 114:
+    return KeyboardButton::f3();
+  case 115:
+    return KeyboardButton::f4();
+  case 116:
+    return KeyboardButton::f5();
+  case 117:
+    return KeyboardButton::f6();
+  case 118:
+    return KeyboardButton::f7();
+  case 119:
+    return KeyboardButton::f8();
+  case 120:
+    return KeyboardButton::f9();
+  case 121:
+    return KeyboardButton::f10();
+  case 122:
+    return KeyboardButton::f11();
+  case 123:
+    return KeyboardButton::f12();
+  case 144:
+    return KeyboardButton::num_lock();
+  case 145:
+    return KeyboardButton::scroll_lock();
+  case 173:
+    return KeyboardButton::ascii_key('-');
+  case 186:
+    return KeyboardButton::ascii_key(';');
+  case 187:
+    return KeyboardButton::ascii_key('=');
+  case 188:
+    return KeyboardButton::ascii_key(',');
+  case 189:
+    return KeyboardButton::ascii_key('-');
+  case 190:
+    return KeyboardButton::ascii_key('.');
+  case 191:
+    return KeyboardButton::ascii_key('?');
+  case 192:
+    return KeyboardButton::ascii_key('`');
+  case 219:
+    return KeyboardButton::ascii_key('[');
+  case 220:
+    return KeyboardButton::ascii_key('\\');
+  case 221:
+    return KeyboardButton::ascii_key(']');
+  case 222:
+    return KeyboardButton::ascii_key('\'');
+  default:
+    return ButtonHandle::none();
+  }
+}
+
+/**
+ * Maps a HTML5 KeyboardEvent.code string to a ButtonHandle.
+ */
+ButtonHandle WebGLGraphicsWindow::
+map_raw_key(const char *code) {
+  static struct {
+    const char *code;
+    ButtonHandle handle;
+  } mappings[] = {
+    //{"Again", KeyboardButton::()},
+    {"AltLeft", KeyboardButton::lalt()},
+    {"AltRight", KeyboardButton::ralt()},
+    {"ArrowDown", KeyboardButton::down()},
+    {"ArrowLeft", KeyboardButton::left()},
+    {"ArrowRight", KeyboardButton::right()},
+    {"ArrowUp", KeyboardButton::up()},
+    {"Backquote", KeyboardButton::ascii_key('`')},
+    {"Backslash", KeyboardButton::ascii_key('\\')},
+    {"Backspace", KeyboardButton::backspace()},
+    {"BracketLeft", KeyboardButton::ascii_key('[')},
+    {"BracketRight", KeyboardButton::ascii_key(']')},
+    //{"BrowserBack", KeyboardButton::()},
+    //{"BrowserFavorites", KeyboardButton::()},
+    //{"BrowserForward", KeyboardButton::()},
+    //{"BrowserRefresh", KeyboardButton::()},
+    //{"BrowserSearch", KeyboardButton::()},
+    //{"BrowserStop", KeyboardButton::()},
+    {"CapsLock", KeyboardButton::caps_lock()},
+    {"Comma", KeyboardButton::ascii_key(',')},
+    {"ContextMenu", KeyboardButton::menu()},
+    {"ControlLeft", KeyboardButton::lcontrol()},
+    {"ControlRight", KeyboardButton::rcontrol()},
+    //{"Convert", KeyboardButton::()},
+    //{"Copy", KeyboardButton::()},
+    //{"Cut", KeyboardButton::()},
+    {"Delete", KeyboardButton::del()},
+    {"Digit0", KeyboardButton::ascii_key('0')},
+    {"Digit1", KeyboardButton::ascii_key('1')},
+    {"Digit2", KeyboardButton::ascii_key('2')},
+    {"Digit3", KeyboardButton::ascii_key('3')},
+    {"Digit4", KeyboardButton::ascii_key('4')},
+    {"Digit5", KeyboardButton::ascii_key('5')},
+    {"Digit6", KeyboardButton::ascii_key('6')},
+    {"Digit7", KeyboardButton::ascii_key('7')},
+    {"Digit8", KeyboardButton::ascii_key('8')},
+    {"Digit9", KeyboardButton::ascii_key('9')},
+    //{"Eject", KeyboardButton::()},
+    {"End", KeyboardButton::end()},
+    {"Enter", KeyboardButton::enter()},
+    {"Equal", KeyboardButton::ascii_key('=')},
+    {"Escape", KeyboardButton::escape()},
+    {"F1", KeyboardButton::f1()},
+    {"F10", KeyboardButton::f10()},
+    {"F11", KeyboardButton::f11()},
+    {"F12", KeyboardButton::f12()},
+    {"F13", KeyboardButton::f13()},
+    {"F14", KeyboardButton::f14()},
+    {"F15", KeyboardButton::f15()},
+    {"F16", KeyboardButton::f16()},
+    //{"F17", KeyboardButton::f17()},
+    //{"F18", KeyboardButton::f18()},
+    //{"F19", KeyboardButton::f19()},
+    {"F2", KeyboardButton::f2()},
+    //{"F20", KeyboardButton::f20()},
+    //{"F21", KeyboardButton::f21()},
+    //{"F22", KeyboardButton::f22()},
+    //{"F23", KeyboardButton::f23()},
+    //{"F24", KeyboardButton::f24()},
+    {"F3", KeyboardButton::f3()},
+    {"F4", KeyboardButton::f4()},
+    {"F5", KeyboardButton::f5()},
+    {"F6", KeyboardButton::f6()},
+    {"F7", KeyboardButton::f7()},
+    {"F8", KeyboardButton::f8()},
+    {"F9", KeyboardButton::f9()},
+    //{"Find", KeyboardButton::()},
+    //{"Fn", KeyboardButton::()},
+    {"Help", KeyboardButton::help()},
+    {"Home", KeyboardButton::home()},
+    {"Insert", KeyboardButton::insert()},
+    //{"IntlBackslash", KeyboardButton::()},
+    //{"IntlRo", KeyboardButton::()},
+    //{"IntlYen", KeyboardButton::()},
+    //{"KanaMode", KeyboardButton::()},
+    {"KeyA", KeyboardButton::ascii_key('a')},
+    {"KeyB", KeyboardButton::ascii_key('b')},
+    {"KeyC", KeyboardButton::ascii_key('c')},
+    {"KeyD", KeyboardButton::ascii_key('d')},
+    {"KeyE", KeyboardButton::ascii_key('e')},
+    {"KeyF", KeyboardButton::ascii_key('f')},
+    {"KeyG", KeyboardButton::ascii_key('g')},
+    {"KeyH", KeyboardButton::ascii_key('h')},
+    {"KeyI", KeyboardButton::ascii_key('i')},
+    {"KeyJ", KeyboardButton::ascii_key('j')},
+    {"KeyK", KeyboardButton::ascii_key('k')},
+    {"KeyL", KeyboardButton::ascii_key('l')},
+    {"KeyM", KeyboardButton::ascii_key('m')},
+    {"KeyN", KeyboardButton::ascii_key('n')},
+    {"KeyO", KeyboardButton::ascii_key('o')},
+    {"KeyP", KeyboardButton::ascii_key('p')},
+    {"KeyQ", KeyboardButton::ascii_key('q')},
+    {"KeyR", KeyboardButton::ascii_key('r')},
+    {"KeyS", KeyboardButton::ascii_key('s')},
+    {"KeyT", KeyboardButton::ascii_key('t')},
+    {"KeyU", KeyboardButton::ascii_key('u')},
+    {"KeyV", KeyboardButton::ascii_key('v')},
+    {"KeyW", KeyboardButton::ascii_key('w')},
+    {"KeyX", KeyboardButton::ascii_key('x')},
+    {"KeyY", KeyboardButton::ascii_key('y')},
+    {"KeyZ", KeyboardButton::ascii_key('z')},
+    //{"Lang1", KeyboardButton::()},
+    //{"Lang2", KeyboardButton::()},
+    //{"LaunchApp1", KeyboardButton::()},
+    //{"MediaPlayPause", KeyboardButton::()},
+    //{"MediaStop", KeyboardButton::()},
+    //{"MediaTrackNext", KeyboardButton::()},
+    //{"MediaTrackPrevious", KeyboardButton::()},
+    {"Minus", KeyboardButton::ascii_key('-')},
+    //{"NonConvert", KeyboardButton::()},
+    {"NumLock", KeyboardButton::num_lock()},
+    {"Numpad0", KeyboardButton::ascii_key('0')},
+    {"Numpad1", KeyboardButton::ascii_key('1')},
+    {"Numpad2", KeyboardButton::ascii_key('2')},
+    {"Numpad3", KeyboardButton::ascii_key('3')},
+    {"Numpad4", KeyboardButton::ascii_key('4')},
+    {"Numpad5", KeyboardButton::ascii_key('5')},
+    {"Numpad6", KeyboardButton::ascii_key('6')},
+    {"Numpad7", KeyboardButton::ascii_key('7')},
+    {"Numpad8", KeyboardButton::ascii_key('8')},
+    {"Numpad9", KeyboardButton::ascii_key('9')},
+    {"NumpadAdd", KeyboardButton::ascii_key('+')},
+    {"NumpadComma", KeyboardButton::ascii_key(',')},
+    {"NumpadDecimal", KeyboardButton::ascii_key('.')},
+    {"NumpadDivide", KeyboardButton::ascii_key('/')},
+    {"NumpadEnter", KeyboardButton::enter()},
+    {"NumpadEqual", KeyboardButton::ascii_key('=')},
+    {"NumpadMultiply", KeyboardButton::ascii_key('*')},
+    {"NumpadSubtract", KeyboardButton::ascii_key('-')},
+    //{"Open", KeyboardButton::()},
+    {"OSLeft", KeyboardButton::lmeta()},
+    {"OSRight", KeyboardButton::rmeta()},
+    {"PageDown", KeyboardButton::page_down()},
+    {"PageUp", KeyboardButton::page_up()},
+    //{"Paste", KeyboardButton::()},
+    {"Pause", KeyboardButton::pause()},
+    {"Period", KeyboardButton::ascii_key('.')},
+    //{"Power", KeyboardButton::()},
+    {"PrintScreen", KeyboardButton::print_screen()},
+    //{"Props", KeyboardButton::()},
+    {"Quote", KeyboardButton::ascii_key('\'')},
+    {"ScrollLock", KeyboardButton::scroll_lock()},
+    //{"Select", KeyboardButton::()},
+    {"Semicolon", KeyboardButton::ascii_key(';')},
+    {"ShiftLeft", KeyboardButton::lshift()},
+    {"ShiftRight", KeyboardButton::rshift()},
+    {"Slash", KeyboardButton::ascii_key('/')},
+    //{"Sleep", KeyboardButton::()},
+    {"Space", KeyboardButton::ascii_key(' ')},
+    {"Tab", KeyboardButton::tab()},
+    //{"Undo", KeyboardButton::()},
+    //{"VolumeDown", KeyboardButton::()},
+    //{"VolumeMute", KeyboardButton::()},
+    //{"VolumeUp", KeyboardButton::()},
+    //{"WakeUp", KeyboardButton::()},
+    {nullptr, ButtonHandle::none()}
+  };
+
+  for (int i = 0; mappings[i].code; ++i) {
+    int cmp = strcmp(mappings[i].code, code);
+    if (cmp == 0) {
+      return mappings[i].handle;
+    } else if (cmp > 0) {
+      // They're in alphabetical order, and we've passed it by, so bail early.
+      return ButtonHandle::none();
+    }
+  }
+
+  return ButtonHandle::none();
+}

+ 87 - 0
panda/src/webgldisplay/webGLGraphicsWindow.h

@@ -0,0 +1,87 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file webGLGraphicsWindow.h
+ * @author rdb
+ * @date 2015-03-31
+ */
+
+#ifndef WEBGLGRAPHICSWINDOW_H
+#define WEBGLGRAPHICSWINDOW_H
+
+#include "pandabase.h"
+
+#include "webGLGraphicsPipe.h"
+#include "graphicsWindow.h"
+
+#include <emscripten/html5.h>
+
+/**
+ * An interface to Emscripten's WebGL interface that represents an HTML5
+ * canvas.
+ */
+class WebGLGraphicsWindow : public GraphicsWindow {
+public:
+  WebGLGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
+                      const std::string &name,
+                      const FrameBufferProperties &fb_prop,
+                      const WindowProperties &win_prop,
+                      int flags,
+                      GraphicsStateGuardian *gsg,
+                      GraphicsOutput *host);
+  virtual ~WebGLGraphicsWindow();
+
+  virtual bool move_pointer(int device, int x, int y);
+  virtual bool begin_frame(FrameMode mode, Thread *current_thread);
+  virtual void end_frame(FrameMode mode, Thread *current_thread);
+  virtual void end_flip();
+
+  virtual void process_events();
+  virtual void set_properties_now(WindowProperties &properties);
+
+protected:
+  virtual void close_window();
+  virtual bool open_window();
+
+private:
+  static EM_BOOL on_fullscreen_event(int type, const EmscriptenFullscreenChangeEvent *event, void *user_data);
+  static EM_BOOL on_pointerlock_event(int type, const EmscriptenPointerlockChangeEvent *event, void *user_data);
+  static EM_BOOL on_visibility_event(int type, const EmscriptenVisibilityChangeEvent *event, void *user_data);
+  static EM_BOOL on_focus_event(int type, const EmscriptenFocusEvent *event, void *user_data);
+  static EM_BOOL on_keyboard_event(int type, const EmscriptenKeyboardEvent *event, void *user_data);
+  static EM_BOOL on_mouse_event(int type, const EmscriptenMouseEvent *event, void *user_data);
+  static EM_BOOL on_wheel_event(int type, const EmscriptenWheelEvent *event, void *user_data);
+
+  static ButtonHandle map_key(int which);
+  static ButtonHandle map_raw_key(const char *code);
+
+  std::string _canvas_id;
+
+  GraphicsWindowInputDevice *_input;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    GraphicsWindow::init_type();
+    register_type(_type_handle, "WebGLGraphicsWindow",
+                  GraphicsWindow::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#include "webGLGraphicsWindow.I"
+
+#endif

+ 2 - 0
tests/putil/test_datagram.py

@@ -1,6 +1,7 @@
 import pytest
 from panda3d import core
 import tempfile
+import sys
 
 # Fixtures for generating interesting datagrams (and verification functions) on
 # the fly...
@@ -178,6 +179,7 @@ def test_file_small(datagram_small):
 
     do_file_test(dg, verify, filename)
 
[email protected](sys.platform == "emscripten", reason="Low-memory environment")
 def test_file_large(datagram_large):
     """This tests DatagramOutputFile/DatagramInputFile on very large datagrams."""
     dg, verify = datagram_large