summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Benjamin Barenblat <bbarenblat@gmail.com>2022-01-17 23:12:32 -0500
committerGravatar Benjamin Barenblat <bbarenblat@gmail.com>2022-01-30 15:55:27 -0500
commitd0e18bdb7924c71cdca8dd983711171d87ef28be (patch)
tree6be11aae0b7c8874e6507b75b4ef26d1353952c7
glplanet, an OpenGL-based planetary rendererHEADmain
glplanet draws Earth like it currently appears from space, putting nighttime areas in shadow and daytime areas in light. It’s modeled after Xplanet (http://xplanet.sourceforge.net/), but whereas Xplanet is entirely a CPU-resident program, glplanet draws using OpenGL. It’s thus much less resource-intensive, particularly when using high-resolution textures.
-rw-r--r--.gitignore21
-rw-r--r--.gitmodules9
-rw-r--r--LICENSE202
l---------build.ninja1
-rw-r--r--buildconf/asan.ninja22
-rw-r--r--buildconf/common.ninja447
-rw-r--r--buildconf/dbg.ninja20
-rw-r--r--buildconf/fastbuild.ninja17
-rw-r--r--buildconf/release.ninja29
-rwxr-xr-xgen/resources_header81
l---------res/clouds.webp1
l---------res/earth.webp1
l---------res/fragment.glsl1
l---------res/vertex.glsl1
-rw-r--r--src/astro.cc182
-rw-r--r--src/astro.h44
-rw-r--r--src/astro_test.cc64
-rw-r--r--src/egl.cc198
-rw-r--r--src/egl.h207
-rw-r--r--src/fragment.glsl40
-rw-r--r--src/gl/README16
-rw-r--r--src/gl/buffer.cc105
-rw-r--r--src/gl/buffer.h206
-rw-r--r--src/gl/draw.cc56
-rw-r--r--src/gl/draw.h217
-rw-r--r--src/gl/error.cc84
-rw-r--r--src/gl/error.h68
-rw-r--r--src/gl/init.cc95
-rw-r--r--src/gl/init.h86
-rw-r--r--src/gl/shader.cc204
-rw-r--r--src/gl/shader.h211
-rw-r--r--src/gl/texture.cc43
-rw-r--r--src/gl/texture.h259
-rw-r--r--src/gl/vertex_array.cc73
-rw-r--r--src/gl/vertex_array.h108
-rw-r--r--src/glplanet.cc170
-rw-r--r--src/glplanet.h31
-rw-r--r--src/main.cc113
-rw-r--r--src/mesh.cc72
-rw-r--r--src/mesh.h80
-rw-r--r--src/mesh_test.cc136
-rw-r--r--src/mvp.maxima56
-rw-r--r--src/scene.cc258
-rw-r--r--src/scene.h87
-rw-r--r--src/undo_xlib_dot_h_namespace_pollution.h553
-rw-r--r--src/util.h35
-rw-r--r--src/vertex.glsl30
-rw-r--r--src/webp.cc80
-rw-r--r--src/webp.h31
-rw-r--r--src/x/connection.cc174
-rw-r--r--src/x/connection.h204
-rw-r--r--src/x/error.cc76
-rw-r--r--src/x/error.h32
-rw-r--r--src/x/event.cc59
-rw-r--r--src/x/event.h137
-rw-r--r--src/x/init.cc33
-rw-r--r--src/x/init.h26
-rw-r--r--src/x/rpc.cc53
-rw-r--r--src/x/rpc.h78
-rw-r--r--src/x/screen.h51
-rw-r--r--src/x/types.h28
-rw-r--r--src/x/util.h33
-rw-r--r--tex/clouds.webpbin0 -> 498578 bytes
-rw-r--r--tex/earth.webpbin0 -> 4091646 bytes
m---------third_party/abseil0
m---------third_party/date0
m---------third_party/glew0
67 files changed, 6135 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c61c593
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+# Copyright 2022 Benjamin Barenblat
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+.ninja_*
+
+res/res.h
+
+*.o
+
+glplanet
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..401e1cf
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "third_party/abseil"]
+ path = third_party/abseil
+ url = https://git.benjamin.barenblat.name/glplanet-abseil.git
+[submodule "third_party/date"]
+ path = third_party/date
+ url = https://github.com/HowardHinnant/date.git
+[submodule "third_party/glew"]
+ path = third_party/glew
+ url = https://github.com/Perlmint/glew-cmake.git
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/build.ninja b/build.ninja
new file mode 120000
index 0000000..d4e407e
--- /dev/null
+++ b/build.ninja
@@ -0,0 +1 @@
+buildconf/fastbuild.ninja \ No newline at end of file
diff --git a/buildconf/asan.ninja b/buildconf/asan.ninja
new file mode 100644
index 0000000..51b26c0
--- /dev/null
+++ b/buildconf/asan.ninja
@@ -0,0 +1,22 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+third_party_cflags = -fsanitize=address
+
+cxxflags = -fsanitize=address -Werror
+third_party_cxxflags = -fsanitize=address
+
+ldflags = -fsanitize=address
+
+subninja buildconf/common.ninja
diff --git a/buildconf/common.ninja b/buildconf/common.ninja
new file mode 100644
index 0000000..0ca45a9
--- /dev/null
+++ b/buildconf/common.ninja
@@ -0,0 +1,447 @@
+# Copyright 2021 Google LLC
+# Copyright 2021, 2022 Benjamin Barenblat
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+ninja_required_version = 1.3
+
+absl = third_party/abseil/absl
+
+common_cflags = -I. -isystem third_party/abseil $
+ -isystem third_party/date/include -isystem third_party/glew/include $
+ -isystem /usr/include/eigen3 -DEIGEN_MPL2_ONLY -DUSE_OS_TZDB $
+ -DUSE_SHELL_API=0 -DGLEW_STATIC -pipe -pthread
+
+third_party_cflags = $common_cflags $third_party_cflags
+
+cxxflags = $common_cflags -std=c++20 -Wall -Wextra -Wno-sign-compare $
+ -fdiagnostics-show-template-tree $cxxflags
+third_party_cxxflags = $common_cflags -std=c++20 $
+ -fdiagnostics-show-template-tree $third_party_cxxflags
+
+# LOCAL ------------------------------------------------------------------------
+cc = gcc
+cxx = g++
+ldflags = -fuse-ld=lld $ldflags
+# ldflags = -fuse-ld=gold $ldflags
+# LOCAL ------------------------------------------------------------------------
+# cc = clang
+# cxx = clang++
+# ldflags = -fuse-ld=lld $ldflags
+# LOCAL ------------------------------------------------------------------------
+
+rule ar
+ command = rm -f $out && ar -rcs $out $in
+ description = Generating $out
+
+rule bin2obj
+ command = ld -r -b binary -o $out $in && objcopy --rename-section $
+ .data=.rodata,alloc,load,readonly,data,contents $out
+ description = Generating $out
+
+rule cc
+ command = $cc -MD -MT $out -MF $out.d -c $in -o $out $cflags
+ description = Compiling $out
+ depfile = $out.d
+ deps = gcc
+
+rule cxx
+ command = $cxx -MD -MT $out -MF $out.d -c $in -o $out $cxxflags
+ description = Compiling $out
+ depfile = $out.d
+ deps = gcc
+
+rule generate_resources_header
+ command = gen/resources_header $resources | clang-format --style=Google >$out
+ description = Generating $out
+
+rule link
+ command = $cxx $ldflags -o $out $in $libs -pthread
+ description = Linking $out
+
+build res/clouds.o: bin2obj res/clouds.webp
+build res/earth.o: bin2obj res/earth.webp
+build res/fragment.o: bin2obj res/fragment.glsl
+build res/vertex.o: bin2obj res/vertex.glsl
+
+build res/res.h: generate_resources_header res | gen/resources_header
+ resources = res/clouds.webp res/earth.webp res/fragment.glsl res/vertex.glsl
+
+build src/astro.o: cxx src/astro.cc
+build src/egl.o: cxx src/egl.cc
+build src/glplanet.o: cxx src/glplanet.cc | res/res.h
+build src/gl/buffer.o: cxx src/gl/buffer.cc
+build src/gl/draw.o: cxx src/gl/draw.cc
+build src/gl/error.o: cxx src/gl/error.cc
+build src/gl/init.o: cxx src/gl/init.cc
+build src/gl/shader.o: cxx src/gl/shader.cc
+build src/gl/texture.o: cxx src/gl/texture.cc
+build src/gl/vertex_array.o: cxx src/gl/vertex_array.cc
+build src/main.o: cxx src/main.cc
+build src/mesh.o: cxx src/mesh.cc
+build src/scene.o: cxx src/scene.cc
+build src/webp.o: cxx src/webp.cc
+build src/x/connection.o: cxx src/x/connection.cc
+build src/x/error.o: cxx src/x/error.cc
+build src/x/event.o: cxx src/x/event.cc
+build src/x/init.o: cxx src/x/init.cc
+build src/x/rpc.o: cxx src/x/rpc.cc
+
+build src/astro_test.o: cxx src/astro_test.cc
+build src/astro_test: link src/astro.o src/astro_test.o $
+ third_party/date/src/tz.a
+ libs = -lgmock_main -lgmock -lgtest -lm
+
+build src/astro_benchmark.o: cxx src/astro_benchmark.cc
+build src/astro_benchmark: link src/astro.o src/astro_benchmark.o $
+ third_party/benchmark/build/src/libbenchmark.a $
+ third_party/benchmark/build/src/libbenchmark_main.a $
+ third_party/date/src/tz.a
+ libs = -lm
+
+build src/mesh_test.o: cxx src/mesh_test.cc
+build src/mesh_test: link src/mesh.o src/mesh_test.o $absl/strings/strings.a $
+ $absl/strings/internal.a $absl/base/base.a $absl/base/spinlock_wait.a $
+ $absl/numeric/int128.a $absl/base/throw_delegate.a $
+ $absl/base/raw_logging_internal.a $absl/base/log_severity.a
+ libs = -lgmock_main -lgmock -lgtest -lrt -lm
+
+build glplanet: link res/clouds.o res/earth.o res/fragment.o res/vertex.o $
+ src/astro.o src/gl/buffer.o src/gl/draw.o src/gl/error.o src/gl/init.o $
+ src/gl/shader.o src/gl/texture.o src/gl/vertex_array.o src/x/connection.o $
+ src/x/error.o src/x/event.o src/x/init.o src/x/rpc.o src/egl.o $
+ src/glplanet.o src/main.o src/mesh.o src/scene.o src/webp.o $
+ $absl/strings/cord.a $absl/strings/cordz_info.a $
+ $absl/strings/cord_internal.a $absl/strings/cordz_functions.a $
+ $absl/profiling/exponential_biased.a $absl/strings/cordz_handle.a $
+ $absl/synchronization/synchronization.a $
+ $absl/synchronization/graphcycles_internal.a $absl/debugging/stacktrace.a $
+ $absl/debugging/symbolize.a $absl/debugging/debugging_internal.a $
+ $absl/debugging/demangle_internal.a $absl/base/malloc_internal.a $
+ $absl/time/time.a $absl/time/civil_time.a $absl/strings/strings.a $
+ $absl/strings/internal.a $absl/base/base.a $absl/base/spinlock_wait.a $
+ $absl/numeric/int128.a $absl/time/time_zone.a $
+ $absl/types/bad_optional_access.a $absl/base/throw_delegate.a $
+ $absl/types/bad_variant_access.a $absl/base/raw_logging_internal.a $
+ $absl/base/log_severity.a third_party/date/src/tz.a $
+ third_party/glew/src/glew.a
+ libs = -lEGL -lGLU -lGL -lX11-xcb -lX11 -lxcb-util -lxcb -lwebp -lm -lrt
+
+# //absl/base
+build $absl/base/base.a: ar $absl/base/internal/cycleclock.o $
+ $absl/base/internal/spinlock.o $absl/base/internal/sysinfo.o $
+ $absl/base/internal/thread_identity.o $
+ $absl/base/internal/unscaledcycleclock.o
+build $absl/base/internal/cycleclock.o: cxx $absl/base/internal/cycleclock.cc
+ cxxflags = $third_party_cxxflags
+build $absl/base/internal/spinlock.o: cxx $absl/base/internal/spinlock.cc
+ cxxflags = $third_party_cxxflags
+build $absl/base/internal/sysinfo.o: cxx $absl/base/internal/sysinfo.cc
+ cxxflags = $third_party_cxxflags
+build $absl/base/internal/thread_identity.o: cxx $
+ $absl/base/internal/thread_identity.cc
+ cxxflags = $third_party_cxxflags
+build $absl/base/internal/unscaledcycleclock.o: cxx $
+ $absl/base/internal/unscaledcycleclock.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/base:log_severity
+build $absl/base/log_severity.a: ar $absl/base/log_severity.o
+build $absl/base/log_severity.o: cxx $absl/base/log_severity.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/base:malloc_internal
+build $absl/base/malloc_internal.a: ar $absl/base/internal/low_level_alloc.o
+build $absl/base/internal/low_level_alloc.o: cxx $
+ $absl/base/internal/low_level_alloc.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/base:raw_logging_internal
+build $absl/base/raw_logging_internal.a: ar $absl/base/internal/raw_logging.o
+build $absl/base/internal/raw_logging.o: cxx $absl/base/internal/raw_logging.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/base:spinlock_wait
+build $absl/base/spinlock_wait.a: ar $absl/base/internal/spinlock_wait.o
+build $absl/base/internal/spinlock_wait.o: cxx $
+ $absl/base/internal/spinlock_wait.cc | $
+ $absl/base/internal/spinlock_linux.inc
+ cxxflags = $third_party_cxxflags
+
+# //absl/base:throw_delegate
+build $absl/base/throw_delegate.a: ar $absl/base/internal/throw_delegate.o
+build $absl/base/internal/throw_delegate.o: cxx $
+ $absl/base/internal/throw_delegate.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/debugging:debugging_internal
+build $absl/debugging/debugging_internal.a: ar $
+ $absl/debugging/internal/address_is_readable.o $
+ $absl/debugging/internal/elf_mem_image.o $
+ $absl/debugging/internal/vdso_support.o
+build $absl/debugging/internal/address_is_readable.o: cxx $
+ $absl/debugging/internal/address_is_readable.cc
+ cxxflags = $third_party_cxxflags
+build $absl/debugging/internal/elf_mem_image.o: cxx $
+ $absl/debugging/internal/elf_mem_image.cc
+ cxxflags = $third_party_cxxflags
+build $absl/debugging/internal/vdso_support.o: cxx $
+ $absl/debugging/internal/vdso_support.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl:debugging:demangle_internal
+build $absl/debugging/demangle_internal.a: ar $
+ $absl/debugging/internal/demangle.o
+build $absl/debugging/internal/demangle.o: cxx $
+ $absl/debugging/internal/demangle.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/debugging:stacktrace
+build $absl/debugging/stacktrace.a: ar $absl/debugging/stacktrace.o
+build $absl/debugging/stacktrace.o: cxx $absl/debugging/stacktrace.cc | $
+ $absl/debugging/internal/stacktrace_config.h $
+ $absl/debugging/internal/stacktrace_x86-inl.inc
+ cxxflags = $third_party_cxxflags
+
+# //absl/debugging:symbolize
+build $absl/debugging/symbolize.a: ar $absl/debugging/symbolize.o
+build $absl/debugging/symbolize.o: cxx $absl/debugging/symbolize.cc | $
+ $absl/debugging/symbolize_elf.inc
+ cxxflags = $third_party_cxxflags
+
+# //absl/numeric:int128
+build $absl/numeric/int128.a: ar $absl/numeric/int128.o
+build $absl/numeric/int128.o: cxx $absl/numeric/int128.cc | $
+ $absl/numeric/int128_have_intrinsic.inc $
+ $absl/numeric/int128_no_intrinsic.inc
+ cxxflags = $third_party_cxxflags
+
+# //absl/profiling:exponential_biased
+build $absl/profiling/exponential_biased.a: ar $
+ $absl/profiling/internal/exponential_biased.o
+build $absl/profiling/internal/exponential_biased.o: cxx $
+ $absl/profiling/internal/exponential_biased.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/strings
+build $absl/strings/strings.a: ar $absl/strings/ascii.o $
+ $absl/strings/charconv.o $absl/strings/escaping.o $
+ $absl/strings/internal/charconv_bigint.o $
+ $absl/strings/internal/charconv_parse.o $absl/strings/internal/memutil.o $
+ $absl/strings/match.o $absl/strings/numbers.o $absl/strings/str_cat.o $
+ $absl/strings/str_replace.o $absl/strings/str_split.o $
+ $absl/strings/string_view.o $absl/strings/substitute.o
+build $absl/strings/ascii.o: cxx $absl/strings/ascii.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/charconv.o: cxx $absl/strings/charconv.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/escaping.o: cxx $absl/strings/escaping.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/charconv_bigint.o: cxx $
+ $absl/strings/internal/charconv_bigint.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/charconv_parse.o: cxx $
+ $absl/strings/internal/charconv_parse.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/memutil.o: cxx $absl/strings/internal/memutil.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/match.o: cxx $absl/strings/match.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/numbers.o: cxx $absl/strings/numbers.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/str_cat.o: cxx $absl/strings/str_cat.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/str_replace.o: cxx $absl/strings/str_replace.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/str_split.o: cxx $absl/strings/str_split.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/string_view.o: cxx $absl/strings/string_view.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/substitute.o: cxx $absl/strings/substitute.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/strings:cord
+build $absl/strings/cord.a: ar $absl/strings/cord.o
+build $absl/strings/cord.o: cxx $absl/strings/cord.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/strings:cord_internal
+build $absl/strings/cord_internal.a: ar $absl/strings/internal/cord_internal.o $
+ $absl/strings/internal/cord_rep_btree.o $
+ $absl/strings/internal/cord_rep_btree_navigator.o $
+ $absl/strings/internal/cord_rep_btree_reader.o $
+ $absl/strings/internal/cord_rep_consume.o $
+ $absl/strings/internal/cord_rep_ring.o
+build $absl/strings/internal/cord_internal.o: cxx $
+ $absl/strings/internal/cord_internal.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/cord_rep_btree.o: cxx $
+ $absl/strings/internal/cord_rep_btree.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/cord_rep_btree_navigator.o: cxx $
+ $absl/strings/internal/cord_rep_btree_navigator.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/cord_rep_btree_reader.o: cxx $
+ $absl/strings/internal/cord_rep_btree_reader.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/cord_rep_consume.o: cxx $
+ $absl/strings/internal/cord_rep_consume.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/cord_rep_ring.o: cxx $
+ $absl/strings/internal/cord_rep_ring.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/strings:cordz_functions
+build $absl/strings/cordz_functions.a: ar $
+ $absl/strings/internal/cordz_functions.o
+build $absl/strings/internal/cordz_functions.o: cxx $
+ $absl/strings/internal/cordz_functions.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/strings:cordz_handle
+build $absl/strings/cordz_handle.a: ar $absl/strings/internal/cordz_handle.o
+build $absl/strings/internal/cordz_handle.o: cxx $
+ $absl/strings/internal/cordz_handle.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/strings:cordz_info
+build $absl/strings/cordz_info.a: ar $absl/strings/internal/cordz_info.o
+build $absl/strings/internal/cordz_info.o: cxx $
+ $absl/strings/internal/cordz_info.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/strings:internal
+build $absl/strings/internal.a: ar $absl/strings/internal/escaping.o $
+ $absl/strings/internal/ostringstream.o $absl/strings/internal/utf8.o
+build $absl/strings/internal/escaping.o: cxx $absl/strings/internal/escaping.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/ostringstream.o: cxx $
+ $absl/strings/internal/ostringstream.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/utf8.o: cxx $absl/strings/internal/utf8.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/synchronization
+build $absl/synchronization/synchronization.a: ar $
+ $absl/synchronization/barrier.o $absl/synchronization/blocking_counter.o $
+ $absl/synchronization/internal/create_thread_identity.o $
+ $absl/synchronization/internal/per_thread_sem.o $
+ $absl/synchronization/internal/waiter.o $
+ $absl/synchronization/notification.o $absl/synchronization/mutex.o
+build $absl/synchronization/barrier.o: cxx $absl/synchronization/barrier.cc
+ cxxflags = $third_party_cxxflags
+build $absl/synchronization/blocking_counter.o: cxx $
+ $absl/synchronization/blocking_counter.cc
+ cxxflags = $third_party_cxxflags
+build $absl/synchronization/internal/create_thread_identity.o: cxx $
+ $absl/synchronization/internal/create_thread_identity.cc
+ cxxflags = $third_party_cxxflags
+build $absl/synchronization/internal/per_thread_sem.o: cxx $
+ $absl/synchronization/internal/per_thread_sem.cc
+ cxxflags = $third_party_cxxflags
+build $absl/synchronization/internal/waiter.o: cxx $
+ $absl/synchronization/internal/waiter.cc
+ cxxflags = $third_party_cxxflags
+build $absl/synchronization/notification.o: cxx $
+ $absl/synchronization/notification.cc
+ cxxflags = $third_party_cxxflags
+build $absl/synchronization/mutex.o: cxx $absl/synchronization/mutex.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/synchronization:graphcycles_internal
+build $absl/synchronization/graphcycles_internal.a: ar $
+ $absl/synchronization/internal/graphcycles.o
+build $absl/synchronization/internal/graphcycles.o: cxx $
+ $absl/synchronization/internal/graphcycles.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/time
+build $absl/time/time.a: ar $absl/time/civil_time.o $absl/time/clock.o $
+ $absl/time/duration.o $absl/time/format.o $absl/time/absl/time.o
+build $absl/time/civil_time.o: cxx $absl/time/civil_time.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/clock.o: cxx $absl/time/clock.cc | $
+ $absl/time/internal/get_current_time_posix.inc
+ cxxflags = $third_party_cxxflags
+build $absl/time/duration.o: cxx $absl/time/duration.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/format.o: cxx $absl/time/format.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/absl/time.o: cxx $absl/time/time.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/time:civil_time
+build $absl/time/civil_time.a: ar $
+ $absl/time/internal/cctz/src/civil_time_detail.o
+build $absl/time/internal/cctz/src/civil_time_detail.o: cxx $
+ $absl/time/internal/cctz/src/civil_time_detail.cc
+ cxxflags = $third_party_cxxflags
+
+# //absl/time:time_zone
+build $absl/time/time_zone.a: ar $
+ $absl/time/internal/cctz/src/time_zone_fixed.o $
+ $absl/time/internal/cctz/src/time_zone_format.o $
+ $absl/time/internal/cctz/src/time_zone_if.o $
+ $absl/time/internal/cctz/src/time_zone_impl.o $
+ $absl/time/internal/cctz/src/time_zone_info.o $
+ $absl/time/internal/cctz/src/time_zone_libc.o $
+ $absl/time/internal/cctz/src/time_zone_lookup.o $
+ $absl/time/internal/cctz/src/time_zone_posix.o $
+ $absl/time/internal/cctz/src/zone_info_source.o
+build $absl/time/internal/cctz/src/time_zone_fixed.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_fixed.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/time_zone_format.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_format.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/time_zone_if.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_if.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/time_zone_impl.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_impl.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/time_zone_info.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_info.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/time_zone_libc.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_libc.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/time_zone_lookup.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_lookup.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/time_zone_posix.o: cxx $
+ $absl/time/internal/cctz/src/time_zone_posix.cc
+ cxxflags = $third_party_cxxflags
+build $absl/time/internal/cctz/src/zone_info_source.o: cxx $
+ $absl/time/internal/cctz/src/zone_info_source.cc
+ cxxflags = $third_party_cxxflags
+
+# // absl/types:bad_optional_access
+build $absl/types/bad_optional_access.a: ar $absl/types/bad_optional_access.o
+build $absl/types/bad_optional_access.o: cxx $absl/types/bad_optional_access.cc
+ cxxflags = $third_party_cxxflags
+
+# // absl/types:bad_variant_access
+build $absl/types/bad_variant_access.a: ar $absl/types/bad_variant_access.o
+build $absl/types/bad_variant_access.o: cxx $absl/types/bad_variant_access.cc
+ cxxflags = $third_party_cxxflags
+
+build third_party/date/src/tz.a: ar third_party/date/src/tz.o
+build third_party/date/src/tz.o: cxx third_party/date/src/tz.cpp
+ cxxflags = $third_party_cflags -DINSTALL=.
+
+build third_party/glew/src/glew.a: ar third_party/glew/src/glew.o
+build third_party/glew/src/glew.o: cc third_party/glew/src/glew.c
+ cflags = $third_party_cflags -DGLEW_EGL
+
+default glplanet
diff --git a/buildconf/dbg.ninja b/buildconf/dbg.ninja
new file mode 100644
index 0000000..b43df19
--- /dev/null
+++ b/buildconf/dbg.ninja
@@ -0,0 +1,20 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+third_party_cflags = -Og -g3
+
+cxxflags = -Og -g3 -Werror
+third_party_cxxflags = -Og -g3
+
+subninja buildconf/common.ninja
diff --git a/buildconf/fastbuild.ninja b/buildconf/fastbuild.ninja
new file mode 100644
index 0000000..c321e30
--- /dev/null
+++ b/buildconf/fastbuild.ninja
@@ -0,0 +1,17 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+cxxflags = -Werror
+
+subninja buildconf/common.ninja
diff --git a/buildconf/release.ninja b/buildconf/release.ninja
new file mode 100644
index 0000000..8f5dd1b
--- /dev/null
+++ b/buildconf/release.ninja
@@ -0,0 +1,29 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+third_party_cflags = -Werror=format -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -O3 -DNDEBUG -fstack-protector-strong -ffunction-sections -fdata-sections -flto
+
+# Don't set -Werror in general; we don't want to risk future warnings appearing
+# in GCC's -Wall/-Wextra and breaking the build.
+#
+# Most of these flags come from Debian's dpkg-buildflags, with the exception
+# that we aim for correctness with -O3, rather than just -O2.
+cxxflags = -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -O3 -DNDEBUG -DGLPLANET_DISABLE_EGL_THREAD_SAFETY_CHECKS -DGLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS -DGLPLANET_DISABLE_AGGRESSIVE_ERROR_CHECKING -fstack-protector-strong -ffunction-sections -fdata-sections
+#-flto
+third_party_cxxflags = -D_FORTIFY_SOURCE=2 -O3 -DNDEBUG -fstack-protector-strong -ffunction-sections -fdata-sections
+#-flto
+
+ldflags = -flto -Wl,-O2 -Wl,-z,relro -Wl,--gc-sections -Wl,--as-needed
+
+subninja buildconf/common.ninja
diff --git a/gen/resources_header b/gen/resources_header
new file mode 100755
index 0000000..e6d6b8d
--- /dev/null
+++ b/gen/resources_header
@@ -0,0 +1,81 @@
+#!/bin/bash
+# Copyright 2021 Benjamin Barenblat
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+set -eu -o pipefail
+
+res=()
+for f in "$@"; do
+ f="${f//./_}"
+ res+=("${f//\//_}")
+done
+
+cat <<EOF
+// This file was generated by gen/resources_header. Edit that script instead.
+#ifndef GLPLANET_RES_RES_H_
+#define GLPLANET_RES_RES_H_
+
+#include <stdint.h>
+
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/types/span.h"
+
+extern "C" {
+
+EOF
+
+for symbol in "${res[@]}"; do
+ echo "extern const char _binary_${symbol}_start;"
+ echo "extern const char _binary_${symbol}_end;"
+ echo
+done
+
+cat <<EOF
+} // extern "C"
+
+namespace glplanet_resources {
+
+EOF
+
+for symbol in "${res[@]}"; do
+ IFS=_ read -r -a parts <<<"$symbol"
+ unset parts[0]
+ function_name=
+ for part in "${parts[@]}"; do
+ function_name+="${part^}"
+ done
+
+ case $symbol in
+ *_webp)
+ echo "inline absl::Span<const uint8_t> $function_name() {"
+ echo ' return absl::MakeConstSpan('
+ echo " reinterpret_cast<const uint8_t*>(&_binary_${symbol}_start),"
+ echo " reinterpret_cast<const uint8_t*>(&_binary_${symbol}_end));"
+ echo '}'
+ ;;
+ *)
+ echo "inline absl::string_view $function_name() {"
+ echo " return absl::string_view(&_binary_${symbol}_start,"
+ echo " &_binary_${symbol}_end);"
+ echo '}'
+ ;;
+ esac
+ echo
+done
+
+cat <<EOF
+} // namespace glplanet_resources
+
+#endif // GLPLANET_RES_RES_H_
+EOF
diff --git a/res/clouds.webp b/res/clouds.webp
new file mode 120000
index 0000000..49ac385
--- /dev/null
+++ b/res/clouds.webp
@@ -0,0 +1 @@
+../tex/clouds.webp \ No newline at end of file
diff --git a/res/earth.webp b/res/earth.webp
new file mode 120000
index 0000000..75154fd
--- /dev/null
+++ b/res/earth.webp
@@ -0,0 +1 @@
+../tex/earth.webp \ No newline at end of file
diff --git a/res/fragment.glsl b/res/fragment.glsl
new file mode 120000
index 0000000..b02ba41
--- /dev/null
+++ b/res/fragment.glsl
@@ -0,0 +1 @@
+../src/fragment.glsl \ No newline at end of file
diff --git a/res/vertex.glsl b/res/vertex.glsl
new file mode 120000
index 0000000..fd99c0e
--- /dev/null
+++ b/res/vertex.glsl
@@ -0,0 +1 @@
+../src/vertex.glsl \ No newline at end of file
diff --git a/src/astro.cc b/src/astro.cc
new file mode 100644
index 0000000..17cd463
--- /dev/null
+++ b/src/astro.cc
@@ -0,0 +1,182 @@
+// Copyright 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/astro.h"
+
+#include <math.h>
+
+#include <chrono>
+#include <utility>
+
+#include "third_party/date/include/date/tz.h"
+
+namespace glplanet {
+
+namespace {
+
+// The J2000 epoch--January 1, 2000, at 12:00:00 Terrestrial Time. This is
+// January 1, 2000, at 11:59:27.816 TAI (because TAI lags TT(TAI) by 32.184 s),
+// or January 1, 2000, at 11:58:55.816 UTC (because TAI-UTC was 32 seconds on
+// January 1, 2000).
+constexpr auto kJ2000Epoch =
+ std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>() +
+ std::chrono::days(42 * 365 + 10 /* leap days */) + std::chrono::hours(11) +
+ std::chrono::minutes(59) + std::chrono::milliseconds(27'816);
+
+constexpr double kMillisecondsPerEphemerisCentury =
+ uint64_t{36'525} * 24 * 60 * 60 * 1'000;
+
+constexpr double kRadiansPerDegree = M_PI / 180.0;
+
+struct CommonValues {
+ // The time, measured in ephemeris centuries from the J2000 epoch.
+ double t;
+
+ double sun_mean_longitude_degrees;
+ double omega;
+ double ecliptic_obliquity;
+};
+
+CommonValues ComputeCommonValues(
+ std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>
+ now) noexcept {
+ const double t =
+ static_cast<double>(std::chrono::duration_cast<std::chrono::milliseconds>(
+ now - kJ2000Epoch)
+ .count()) /
+ kMillisecondsPerEphemerisCentury;
+
+ // Digit groupings in these magic constants match those in Meeus for easy
+ // auditing.
+ const double mean_longitude_degrees =
+ (0.000'3032 * t + 36'000.769'83) * t + 280.46646;
+ const double omega =
+ (((2.222222222222222e-6 * t + 0.002'0708) * t + -1934.136'261) * t +
+ 125.04452) *
+ kRadiansPerDegree;
+
+ // From Meeus (22.2) and (25.8).
+ const double ecliptic_obliquity_degrees =
+ ((5.036111111111111e-7 * t + -1.6388888888888888e-7) * t +
+ -1.3004166666666667e-2) *
+ t +
+ 0.00256 * cos(omega) + // (25.8)
+ 23.43929111111111;
+
+ return {
+ .t = t,
+ .sun_mean_longitude_degrees = mean_longitude_degrees,
+ .omega = omega,
+ .ecliptic_obliquity = ecliptic_obliquity_degrees * kRadiansPerDegree,
+ };
+}
+
+std::pair<double, double> SunEquatorialPositionWithCommonValues(
+ const CommonValues& common) noexcept {
+ // Reference: Jean Meeus, _Astronomical Algorithms_, 2nd edition,
+ // Willmann-Bell, 1998 (ISBN 0-943396-61-1), chapter 25.
+
+ const auto& [t, mean_longitude_degrees, omega, ecliptic_obliquity] = common;
+
+ // Again, digit groupings match Meeus.
+ const double mean_anomaly_degrees =
+ (0.000'1537 * t + 35'999.050'29) * t + 357.52911;
+ const double equation_of_center_degrees =
+ 0.000'289 * sin(mean_anomaly_degrees * (M_PI / 60.0)) +
+ (-0.000'101 * t + 0.019'993) * sin(mean_anomaly_degrees * (M_PI / 90.0)) +
+ ((-0.000'014 * t - 0.004'817) * t + 1.914'602) *
+ sin(mean_anomaly_degrees * (M_PI / 180.0));
+ const double apparent_longitude_degrees = -0.00478 * sin(omega) - 0.00569 +
+ equation_of_center_degrees +
+ mean_longitude_degrees;
+
+ const double apparent_longitude =
+ apparent_longitude_degrees * kRadiansPerDegree;
+
+ // Per Meeus, latitude never exceeds 1.2 arcseconds = 0.0003 degrees,
+ // within margin of error. We therefore have:
+ const double right_ascension_radians =
+ atan2(cos(ecliptic_obliquity) * sin(apparent_longitude),
+ cos(apparent_longitude));
+ const double declination_radians =
+ asin(sin(ecliptic_obliquity) * sin(apparent_longitude));
+
+ return {right_ascension_radians, declination_radians};
+}
+
+double ApparentSiderealTimeAtGreenwichWithCommonValues(
+ const CommonValues& common) noexcept {
+ // Reference: Meeus, page 88.
+
+ const auto& [t, mean_longitude_degrees, omega, ecliptic_obliquity] = common;
+
+ const double moon_mean_longitude_degrees = 481'267.8831 * t + 218.3165;
+ const double longitude_nutation_degrees =
+ 5.833333333333333e-5 * sin(2 * omega) +
+ -6.38888888888888e-5 * sin(moon_mean_longitude_degrees * M_PI / 90.0) +
+ -3.6666666666666666e-4 * sin(mean_longitude_degrees * M_PI / 90.0) +
+ -4.7777777777777777e-3 * sin(omega);
+ double greenwich_apparent_sidereal_time_degrees =
+ ((-2.5833118057349522e-8 * t + 0.000'387'933) * t + 13185000.770053742) *
+ t +
+ longitude_nutation_degrees * cos(ecliptic_obliquity) +
+ 280.460'618'37; // (12.4)
+ greenwich_apparent_sidereal_time_degrees =
+ fmod(greenwich_apparent_sidereal_time_degrees, 360.0);
+ if (greenwich_apparent_sidereal_time_degrees < -180.0) {
+ greenwich_apparent_sidereal_time_degrees += 360.0;
+ }
+
+ return greenwich_apparent_sidereal_time_degrees * kRadiansPerDegree;
+}
+
+} // namespace
+
+std::pair<double, double> SunEquatorialPosition(
+ const std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>
+ now) noexcept {
+ return SunEquatorialPositionWithCommonValues(ComputeCommonValues(now));
+}
+
+double ApparentSiderealTimeAtGreenwich(
+ std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>
+ now) noexcept {
+ return ApparentSiderealTimeAtGreenwichWithCommonValues(
+ ComputeCommonValues(now));
+}
+
+std::pair<double, double> HighNoonLocation(
+ std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>
+ now) noexcept {
+ CommonValues common = ComputeCommonValues(now);
+ const auto& [right_ascension, declination] =
+ SunEquatorialPositionWithCommonValues(common);
+ double greenwich_apparent_sidereal_time =
+ ApparentSiderealTimeAtGreenwichWithCommonValues(common);
+
+ // The declination is the latitude.
+ const double latitude = declination;
+
+ // Computing the longitude is only a bit more complex. We're sitting right
+ // under the sun, so our local hour angle is 0, giving
+ //
+ // east longitude = right ascension - apparent sidereal time at Greenwich
+ //
+ // (cf. Meeus, page 92).
+ const double longitude = right_ascension - greenwich_apparent_sidereal_time;
+
+ return {longitude, latitude};
+}
+
+} // namespace glplanet
diff --git a/src/astro.h b/src/astro.h
new file mode 100644
index 0000000..26dcc04
--- /dev/null
+++ b/src/astro.h
@@ -0,0 +1,44 @@
+// Copyright 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_ASTRO_H_
+#define GLPLANET_SRC_ASTRO_H_
+
+#include <chrono>
+#include <utility>
+
+#include "third_party/date/include/date/tz.h"
+
+namespace glplanet {
+
+// The apparent right ascension and declination of the sun, in radians. Accurate
+// to 0.01 degrees.
+std::pair<double, double> SunEquatorialPosition(
+ std::chrono::time_point<date::tai_clock,
+ std::chrono::milliseconds>) noexcept;
+
+// The apparent sidereal time at Greenwich, in radians.
+double ApparentSiderealTimeAtGreenwich(
+ std::chrono::time_point<date::tai_clock,
+ std::chrono::milliseconds>) noexcept;
+
+// The east longitude and north latitude of the location observing a Sun transit
+// (i.e., the location at which it's high noon), in radians.
+std::pair<double, double> HighNoonLocation(
+ std::chrono::time_point<date::tai_clock,
+ std::chrono::milliseconds>) noexcept;
+
+} // namespace glplanet
+
+#endif // GLPLANET_SRC_ASTRO_H_
diff --git a/src/astro_test.cc b/src/astro_test.cc
new file mode 100644
index 0000000..2fc1cb8
--- /dev/null
+++ b/src/astro_test.cc
@@ -0,0 +1,64 @@
+// Copyright 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/astro.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <chrono>
+
+#include "third_party/date/include/date/tz.h"
+
+namespace glplanet {
+namespace {
+
+using ::testing::DoubleNear;
+using ::testing::Pair;
+
+TEST(SunEquatorialPositionTest, MeeusExercise25A) {
+ // October 13, 1992, at 00:00:00 TDT (T = -0.072'183'436)
+ constexpr auto t =
+ std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>() +
+ std::chrono::days(34 * 365 + 8 /* leap days */) +
+ std::chrono::days(31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 11) +
+ std::chrono::hours(23) + std::chrono::minutes(59) +
+ std::chrono::milliseconds(27'816);
+ EXPECT_THAT(SunEquatorialPosition(t), Pair(DoubleNear(-2.82078673, 1e-8),
+ DoubleNear(-0.1358751, 1e-8)));
+}
+
+TEST(ApparentSiderealTimeAtGreenwichTest, MeeusExercise12A) {
+ // April 10, 1987, at 00:00:00 UT (T = -0.127'296'372'348)
+ constexpr auto t =
+ std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>() +
+ std::chrono::days(29 * 365 + 7 /* leap days */) +
+ std::chrono::days(31 + 28 + 31 + 8) + std::chrono::hours(23) +
+ std::chrono::minutes(59) + std::chrono::milliseconds(27'816);
+ EXPECT_THAT(ApparentSiderealTimeAtGreenwich(t), DoubleNear(-2.832805, 1e-6));
+}
+
+TEST(HighNoonLocationTest, MarchEquinox) {
+ // March 20, 2022 15:29:52.331 UT
+ constexpr auto t =
+ std::chrono::time_point<date::tai_clock, std::chrono::milliseconds>() +
+ std::chrono::days(64 * 365 + 16 /* leap days */) +
+ std::chrono::days(31 + 28 + 19) + std::chrono::hours(15) +
+ std::chrono::minutes(29) + std::chrono::milliseconds(24'515);
+ EXPECT_THAT(HighNoonLocation(t),
+ Pair(DoubleNear(-0.883655431852, 1e-10), DoubleNear(0.0, 1e-10)));
+}
+
+} // namespace
+} // namespace glplanet
diff --git a/src/egl.cc b/src/egl.cc
new file mode 100644
index 0000000..c24815b
--- /dev/null
+++ b/src/egl.cc
@@ -0,0 +1,198 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/egl.h"
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <X11/Xlib.h>
+
+#include <exception>
+#include <new>
+#include <stdexcept>
+#include <utility>
+#include <vector>
+
+#include "src/undo_xlib_dot_h_namespace_pollution.h"
+//
+
+#include "src/util.h"
+#include "third_party/abseil/absl/container/btree_map.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace egl {
+
+namespace {
+
+// Converts an EGL error into a standard C++ exception.
+std::exception ExceptionFromErrorCode(EGLint error) noexcept {
+ DCHECK(error != EGL_SUCCESS);
+ switch (error) {
+ case EGL_NOT_INITIALIZED:
+ return std::logic_error("EGL: uninitialized");
+ case EGL_BAD_ACCESS:
+ return std::logic_error("EGL: bad access");
+ case EGL_BAD_ALLOC:
+ return std::bad_alloc();
+ case EGL_BAD_ATTRIBUTE:
+ return std::invalid_argument(
+ "EGL: unrecognized attribute or attribute value");
+ case EGL_BAD_CONTEXT:
+ return std::invalid_argument("EGL: invalid context");
+ case EGL_BAD_CONFIG:
+ return std::invalid_argument("EGL: invalid configuration");
+ case EGL_BAD_CURRENT_SURFACE:
+ return std::invalid_argument("EGL: current surface is no longer valid");
+ case EGL_BAD_DISPLAY:
+ return std::invalid_argument("EGL: invalid display");
+ case EGL_BAD_SURFACE:
+ return std::invalid_argument("EGL: invalid surface");
+ case EGL_BAD_MATCH:
+ return std::invalid_argument("EGL: inconsistent arguments");
+ case EGL_BAD_PARAMETER:
+ return std::invalid_argument("EGL: invalid argument");
+ case EGL_BAD_NATIVE_PIXMAP:
+ return std::invalid_argument("EGL: invalid native pixmap");
+ case EGL_BAD_NATIVE_WINDOW:
+ return std::invalid_argument("EGL: invalid native window");
+ case EGL_CONTEXT_LOST:
+ return std::runtime_error(
+ "EGL: context lost due to power management event");
+ default:
+ return std::runtime_error("EGL: error");
+ }
+}
+
+// Checks the specified result against `error`. If it is indeed `error`, gets
+// the associated EGL error and throws it as an exception.
+template <typename T>
+T SentinelCheckedCall(T error, T result) {
+ if (result == error) {
+ throw ExceptionFromErrorCode(eglGetError());
+ }
+ return result;
+}
+
+// A variant of SentinelCheckedCall that checks for `false` returns.
+void CheckedCall(bool result) { SentinelCheckedCall(false, result); }
+
+// Converts an egl::AttributeList into the format expected by EGL's C APIs.
+std::vector<EGLint> MarshalAttributeList(
+ const absl::btree_map<EGLint, EGLint>& attribs) {
+ std::vector<EGLint> r;
+ r.reserve(2 * attribs.size() + 1);
+ for (auto& [key, val] : attribs) {
+ r.push_back(key);
+ r.push_back(val);
+ }
+ r.push_back(EGL_NONE);
+ return r;
+}
+
+} // namespace
+
+int Configuration::Get(Attribute attribute) const {
+ EGLint r;
+ CheckedCall(eglGetConfigAttrib(display_, config_, attribute, &r));
+ return r;
+}
+
+void Surface::SwapBuffers() { CheckedCall(eglSwapBuffers(display_, surface_)); }
+
+Display::Display(::Display* xlib_display)
+ : display_(SentinelCheckedCall(
+ EGL_NO_DISPLAY,
+ eglGetPlatformDisplay(EGL_PLATFORM_X11_KHR, xlib_display,
+ /*attrib_list=*/nullptr))) {
+ EGLint major, minor;
+ CheckedCall(eglInitialize(display_, &major, &minor));
+}
+
+Display::~Display() noexcept {
+ // This should always succeed, since display_ is guaranteed valid.
+ DCHECK(eglTerminate(display_));
+}
+
+std::vector<Configuration> Display::GetConfigurations(
+ const std::vector<std::pair<Attribute, int>>& filter_attributes,
+ absl::optional<int> limit) const {
+ // Create the actual attribute list that we're going to pass to
+ // eglChooseConfig.
+ std::vector<EGLint> marshaled_attributes;
+ marshaled_attributes.reserve(2 * filter_attributes.size() + 1);
+ for (auto& [key, value] : filter_attributes) {
+ marshaled_attributes.push_back(key);
+ marshaled_attributes.push_back(value);
+ }
+ marshaled_attributes.push_back(EGL_NONE);
+
+ // Determine how many configs we want.
+ EGLint num_configs;
+ if (limit.has_value()) {
+ num_configs = *limit;
+ } else {
+ CheckedCall(eglChooseConfig(display_, marshaled_attributes.data(),
+ /*configs=*/nullptr,
+ /*config_size=*/0, &num_configs));
+ }
+
+ // Actually get the configs.
+ std::vector<EGLConfig> egl_configs(num_configs);
+ CheckedCall(eglChooseConfig(display_, marshaled_attributes.data(),
+ egl_configs.data(), egl_configs.size(),
+ &num_configs));
+
+ // Wrap the configs in Configuration objects.
+ std::vector<Configuration> r;
+ r.reserve(num_configs);
+ for (auto egl_config : egl_configs) {
+ r.push_back(Configuration(display_, egl_config));
+ }
+
+ return r;
+}
+
+Surface Display::CreateWindowSurface(const Configuration& config,
+ ::Window window) {
+ return Surface(
+ display_,
+ SentinelCheckedCall(EGL_NO_SURFACE, eglCreatePlatformWindowSurface(
+ display_, config.config_, &window,
+ /*attrib_list=*/nullptr)));
+}
+
+Context Display::CreateContext(const Configuration& config, Api api, int major,
+ int minor) {
+ absl::btree_map<EGLint, EGLint> attributes = {
+ {EGL_CONTEXT_MAJOR_VERSION, major}, {EGL_CONTEXT_MINOR_VERSION, minor}};
+#ifndef NDEBUG
+ attributes[EGL_CONTEXT_OPENGL_DEBUG] = EGL_TRUE;
+#endif
+
+ CheckedCall(eglBindAPI(FromEnum(api)));
+ return Context(
+ display_, SentinelCheckedCall(
+ EGL_NO_CONTEXT,
+ eglCreateContext(display_, config.config_,
+ /*share_context=*/EGL_NO_CONTEXT,
+ MarshalAttributeList(attributes).data())));
+}
+
+void BindContext(Surface& surface, Context& context) {
+ DCHECK(surface.display_ == context.display_);
+ CheckedCall(eglMakeCurrent(surface.display_, /*draw=*/surface.surface_,
+ /*read=*/surface.surface_, context.context_));
+}
+
+} // namespace egl
diff --git a/src/egl.h b/src/egl.h
new file mode 100644
index 0000000..0f16f88
--- /dev/null
+++ b/src/egl.h
@@ -0,0 +1,207 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// An idiomatic C++ interface to EGL 1.5.
+
+#ifndef GLPLANET_SRC_EGL_H_
+#define GLPLANET_SRC_EGL_H_
+
+#include <EGL/egl.h>
+#include <X11/Xlib.h>
+
+#include <utility>
+#include <vector>
+
+#include "src/undo_xlib_dot_h_namespace_pollution.h"
+//
+
+#include "src/util.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace egl {
+
+class Context;
+class Display;
+class Surface;
+
+// Makes a Context active in the current thread.
+void BindContext(Surface&, Context&);
+
+// Configuration attributes.
+enum Attribute : EGLint {
+ kBufferSize = EGL_BUFFER_SIZE,
+
+ kRedSize = EGL_RED_SIZE,
+ kGreenSize = EGL_GREEN_SIZE,
+ kBlueSize = EGL_BLUE_SIZE,
+ kLuminanceSize = EGL_LUMINANCE_SIZE,
+ kAlphaSize = EGL_ALPHA_SIZE,
+ kAlphaMaskSize = EGL_ALPHA_MASK_SIZE,
+
+ kBindToTextureRgb = EGL_BIND_TO_TEXTURE_RGB,
+ kBindToTextureRgba = EGL_BIND_TO_TEXTURE_RGBA,
+ kColorBufferType = EGL_COLOR_BUFFER_TYPE,
+ kConfigCaveat = EGL_CONFIG_CAVEAT,
+ kConfigId = EGL_CONFIG_ID,
+ kConformantMask = EGL_CONFORMANT,
+ kDepthSize = EGL_DEPTH_SIZE,
+ kLevel = EGL_LEVEL,
+ kMatchNativePixmap = EGL_MATCH_NATIVE_PIXMAP,
+ kMaxSwapInterval = EGL_MAX_SWAP_INTERVAL,
+ kMinSwapInterval = EGL_MIN_SWAP_INTERVAL,
+ kNativeRenderable = EGL_NATIVE_RENDERABLE,
+ kNativeVisualId = EGL_NATIVE_VISUAL_ID,
+ kNativeVisualType = EGL_NATIVE_VISUAL_TYPE,
+ kSampleBuffers = EGL_SAMPLE_BUFFERS,
+ kSamples = EGL_SAMPLES,
+ kStencilSize = EGL_STENCIL_SIZE,
+ kSurfaceTypeMask = EGL_SURFACE_TYPE,
+ kTransparentType = EGL_TRANSPARENT_TYPE,
+ kTransparentRedValue = EGL_TRANSPARENT_RED_VALUE,
+ kTransparentGreenValue = EGL_TRANSPARENT_GREEN_VALUE,
+ kTransparentBlueValue = EGL_TRANSPARENT_BLUE_VALUE,
+
+ kRenderableTypeMask = EGL_RENDERABLE_TYPE,
+};
+
+// A "don't care" value for use in attribute lists.
+constexpr int kDontCare = EGL_DONT_CARE;
+
+enum class Api : EGLenum {
+ kOpenglEs = EGL_OPENGL_ES_API,
+ kOpenvg = EGL_OPENVG_API,
+ kOpengl = EGL_OPENGL_API,
+};
+
+enum ApiMask : EGLint {
+ kOpenglEsBit = EGL_OPENGL_ES_BIT,
+ kOpenVgBit = EGL_OPENVG_BIT,
+ kOpenglEs2Bit = EGL_OPENGL_ES2_BIT,
+ kOpenglBit = EGL_OPENGL_BIT,
+};
+
+// The format, type, and size of the buffers for a drawable.
+//
+// This class is thread-safe.
+class Configuration final {
+ public:
+ Configuration(const Configuration&) = default;
+ Configuration& operator=(const Configuration&) = default;
+ Configuration(Configuration&&) noexcept = default;
+ Configuration& operator=(Configuration&&) noexcept = default;
+
+ int Get(Attribute attribute) const;
+
+ private:
+ friend class Display;
+
+ explicit Configuration(EGLDisplay display, EGLConfig config) noexcept
+ : display_(display), config_(config) {}
+
+ EGLDisplay display_;
+ EGLConfig config_;
+};
+
+// A rendering surface.
+//
+// This class inherits the thread-safety properties of the Display that
+// constructed it.
+class Surface final {
+ public:
+ Surface(const Surface&) = default;
+ Surface& operator=(const Surface&) = default;
+ Surface(Surface&&) noexcept = default;
+ Surface& operator=(Surface&&) noexcept = default;
+
+ void SwapBuffers();
+
+ private:
+ friend class Display;
+ friend void BindContext(Surface&, Context&);
+
+ explicit Surface(EGLDisplay display, EGLSurface surface) noexcept
+ : display_(display), surface_(surface) {}
+
+ EGLDisplay display_;
+ EGLSurface surface_;
+};
+
+// A rendering context.
+//
+// This class inherits the thread-safety properties of the Display that
+// constructed it.
+class Context final {
+ public:
+ Context(Context&&) noexcept = default;
+ Context& operator=(Context&&) noexcept = default;
+
+ ~Context() noexcept {
+ // This should always succeed, since display_ and context_ are guaranteed
+ // valid.
+ DCHECK(eglDestroyContext(display_, context_));
+ }
+
+ private:
+ friend class Display;
+ friend void BindContext(Surface&, Context&);
+
+ explicit Context(EGLDisplay display, EGLContext context) noexcept
+ : display_(display), context_(context) {}
+
+ EGLDisplay display_;
+ EGLContext context_;
+};
+
+// The abstract display on which graphics are drawn.
+//
+// This class inherits the thread-safety properties of the underlying Xlib
+// Display with which it is constructed.
+class Display final {
+ public:
+ explicit Display(::Display* xlib_display);
+
+ Display(Display&&) noexcept = default;
+ Display& operator=(Display&&) noexcept = default;
+
+ ~Display() noexcept;
+
+ // Gets Configurations that match the specified attributes. If a limit is
+ // specified, no more than that many Configurations will be returned.
+ //
+ // The returned Configurations hold references into this object and must not
+ // outlive it.
+ std::vector<Configuration> GetConfigurations(
+ const std::vector<std::pair<Attribute, int>>& filter_attributes,
+ absl::optional<int> limit = absl::nullopt) const;
+
+ // Creates a rendering surface on a window.
+ //
+ // The returned Surface holds a reference into this Display and must not
+ // outlive it.
+ Surface CreateWindowSurface(const Configuration& config, ::Window window);
+
+ // Creates an OpenGL context.
+ //
+ // The returned Context holds a reference into this Display and must not
+ // outlive it.
+ Context CreateContext(const Configuration& config, Api api, int major,
+ int minor);
+
+ private:
+ EGLDisplay display_;
+};
+
+} // namespace egl
+
+#endif // GLPLANET_SRC_EGL_H_
diff --git a/src/fragment.glsl b/src/fragment.glsl
new file mode 100644
index 0000000..fcdc096
--- /dev/null
+++ b/src/fragment.glsl
@@ -0,0 +1,40 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#version 150
+
+// The color of sunlight (5800 K) in sRGB.
+const vec4 kSunColor = vec4(1.0, 0.9411765, 0.9137255, 1.0);
+
+in vec3 model_cartesian_position; // unit vector pointing toward current point
+in vec2 texture_coordinate;
+
+uniform sampler2D planet;
+uniform sampler2D clouds;
+uniform vec3 sun_direction; // unit vector pointing toward sun
+
+out vec4 out_color;
+
+void main() {
+ // Compute diffuse illumination from the Sun. We assume all rays are parallel,
+ // which isn't true, but at Earth scales, it causes a maximum error of about
+ // 0.003 degrees, well within our error budget.
+ float diffuse = max(dot(model_cartesian_position, sun_direction), 0.0);
+
+ // The planet is a mixture of the ground texture and the cloud texture. Assume
+ // clouds reflect sunlight; the ground texture needs no color adjustment,
+ // since it's already colored as if being hit by sunlight.
+ out_color = diffuse * mix(texture(planet, texture_coordinate), kSunColor,
+ texture(clouds, texture_coordinate).r);
+}
diff --git a/src/gl/README b/src/gl/README
new file mode 100644
index 0000000..9e7aed4
--- /dev/null
+++ b/src/gl/README
@@ -0,0 +1,16 @@
+This directory defines an idiomatic C++ interface to parts of OpenGL.
+
+This library requires OpenGL 3.2 with the following extensions:
+ - ARB_texture_storage (or OpenGL >=4.2)
+ - ARB_direct_state_access (or OpenGL >=4.5)
+
+OpenGL is an inherently thread-hostile interface (it uses thread-local state).
+By default, this library will throw if it detects it is being used in a
+thread-unsafe way. However, this checking does carry a performance cost, so
+defining GLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS disables these checks.
+
+By default, this library throws OpenGL errors as soon as they occur. This makes
+debugging easier, since it associates errors more closely with the operations to
+which they correspond. However, this bookkeeping carries a performance cost;
+defining GLPLANET_DISABLE_AGGRESSIVE_ERROR_CHECKING will reduce it to the
+absolute minimum required for correctness.
diff --git a/src/gl/buffer.cc b/src/gl/buffer.cc
new file mode 100644
index 0000000..a56e36a
--- /dev/null
+++ b/src/gl/buffer.cc
@@ -0,0 +1,105 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/gl/buffer.h"
+
+#include <stdint.h>
+
+#include <stdexcept>
+
+#include "src/gl/error.h"
+#include "src/util.h"
+#include "third_party/abseil/absl/strings/substitute.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+namespace {
+
+constexpr uint16_t Pack(Buffer::AccessFrequency frequency,
+ Buffer::AccessNature nature) noexcept {
+ static_assert(sizeof(Buffer::AccessFrequency) == 1);
+ static_assert(sizeof(Buffer::AccessNature) == 1);
+ return FromEnum(frequency) << 8 | FromEnum(nature);
+}
+
+GLenum CombineFrequencyAndNature(Buffer::AccessFrequency frequency,
+ Buffer::AccessNature nature) {
+ using F = ::gl::Buffer::AccessFrequency;
+ using N = ::gl::Buffer::AccessNature;
+ switch (Pack(frequency, nature)) {
+ case Pack(F::kStream, N::kDraw):
+ return GL_STREAM_DRAW;
+ case Pack(F::kStream, N::kRead):
+ return GL_STREAM_READ;
+ case Pack(F::kStream, N::kCopy):
+ return GL_STREAM_COPY;
+
+ case Pack(F::kStatic, N::kDraw):
+ return GL_STATIC_DRAW;
+ case Pack(F::kStatic, N::kRead):
+ return GL_STATIC_READ;
+ case Pack(F::kStatic, N::kCopy):
+ return GL_STATIC_COPY;
+
+ case Pack(F::kDynamic, N::kDraw):
+ return GL_DYNAMIC_DRAW;
+ case Pack(F::kDynamic, N::kRead):
+ return GL_DYNAMIC_READ;
+ case Pack(F::kDynamic, N::kCopy):
+ return GL_DYNAMIC_COPY;
+
+ default:
+ throw std::invalid_argument(absl::Substitute(
+ "GL: invalid access frequency and/or nature ($0, $1)",
+ FromEnum(frequency), FromEnum(nature)));
+ }
+}
+
+} // namespace
+
+Buffer::Buffer(const Buffer& other)
+ : size_bytes_(other.size_bytes_),
+ frequency_(other.frequency_),
+ nature_(other.nature_) {
+ gl_internal::CheckThreadSafety();
+
+ glCreateBuffers(1, &buffer_);
+ gl_internal::UnnecessaryErrorCheck();
+
+ glNamedBufferData(buffer_, size_bytes_, /*data=*/nullptr,
+ CombineFrequencyAndNature(frequency_, nature_));
+ // An error check is necessary here to detect allocation failure, but it can
+ // be folded into a future error check.
+ gl_internal::UnnecessaryErrorCheck();
+
+ glCopyNamedBufferSubData(other.buffer_, buffer_, /*readOffset=*/0,
+ /*writeOffset=*/0, other.size_bytes_);
+ // This error check is necessary because other might be mapped.
+ gl_internal::ErrorCheck();
+
+ fprintf(stderr, "BUFFER COPY\n");
+}
+
+void Buffer::SetData(const void* data, int size_bytes,
+ AccessFrequency frequency, AccessNature nature) {
+ gl_internal::CheckThreadSafety();
+ glNamedBufferData(buffer_, size_bytes, data,
+ CombineFrequencyAndNature(frequency, nature));
+ // This error check is necessary to detect allocation failure.
+ gl_internal::ErrorCheck();
+ size_bytes_ = size_bytes;
+}
+
+} // namespace gl
diff --git a/src/gl/buffer.h b/src/gl/buffer.h
new file mode 100644
index 0000000..439533b
--- /dev/null
+++ b/src/gl/buffer.h
@@ -0,0 +1,206 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Buffer objects.
+
+#ifndef GLPLANET_SRC_GL_BUFFER_H_
+#define GLPLANET_SRC_GL_BUFFER_H_
+
+#include <assert.h>
+#include <stdint.h>
+
+#include <concepts>
+#include <type_traits>
+#include <utility>
+
+#include "src/gl/error.h"
+#include "third_party/abseil/absl/types/span.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+// A byte buffer in GPU memory.
+class Buffer final {
+ public:
+ // The frequency with which data in the buffer will be accessed.
+ enum class AccessFrequency : uint8_t { kStatic, kStream, kDynamic };
+
+ // The type of that access.
+ enum class AccessNature : uint8_t { kDraw, kRead, kCopy };
+
+ explicit Buffer() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking))
+ : size_bytes_(0),
+ frequency_(AccessFrequency::kStatic),
+ nature_(AccessNature::kDraw) {
+ gl_internal::CheckThreadSafety();
+ glCreateBuffers(1, &buffer_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ Buffer(const Buffer&);
+
+ Buffer& operator=(const Buffer& other) {
+ if (this != &other) {
+ Buffer other2(other);
+ swap(*this, other2);
+ }
+ return *this;
+ }
+
+ Buffer(Buffer&& other) noexcept
+ : buffer_(0),
+ size_bytes_(0),
+ frequency_(AccessFrequency::kStatic),
+ nature_(AccessNature::kDraw) {
+ *this = std::move(other);
+ }
+
+ Buffer& operator=(Buffer&& other) noexcept {
+ swap(*this, other);
+ return *this;
+ }
+
+ ~Buffer() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glDeleteBuffers(1, &buffer_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ friend void swap(Buffer& left, Buffer& right) noexcept {
+ using ::std::swap;
+ swap(left.buffer_, right.buffer_);
+ swap(left.size_bytes_, right.size_bytes_);
+ swap(left.frequency_, right.frequency_);
+ swap(left.nature_, right.nature_);
+ }
+
+ // Loads data into the buffer.
+ void SetData(const void*, int size_bytes, AccessFrequency, AccessNature);
+
+ int size() const noexcept { return size_bytes_; }
+
+ // The GL identifier for this buffer.
+ unsigned int id() const noexcept { return buffer_; }
+
+ private:
+ GLuint buffer_;
+ int size_bytes_;
+ AccessFrequency frequency_;
+ AccessNature nature_;
+};
+
+// A vertex buffer object.
+class VertexBuffer final {
+ public:
+ explicit VertexBuffer() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) =
+ default;
+
+ // A shorthand to construct the buffer and load data into it in one step.
+ template <typename T>
+ explicit VertexBuffer(absl::Span<const T> data,
+ Buffer::AccessFrequency frequency,
+ Buffer::AccessNature nature) {
+ SetData(data, frequency, nature);
+ }
+
+ VertexBuffer(const VertexBuffer&) = default;
+ VertexBuffer& operator=(const VertexBuffer&) = default;
+ VertexBuffer(VertexBuffer&&) = default;
+ VertexBuffer& operator=(VertexBuffer&&) = default;
+
+ // Loads data into the buffer.
+ template <typename T>
+ void SetData(absl::Span<const T> data, Buffer::AccessFrequency frequency,
+ Buffer::AccessNature nature) {
+ buffer_.SetData(data.data(), data.size() * sizeof(T), frequency, nature);
+ element_size_ = sizeof(T);
+ }
+
+ // The number of vertices in this buffer.
+ int size() const noexcept {
+ assert(buffer_.size() % element_size() == 0);
+ return buffer_.size() / element_size();
+ }
+
+ // The size, in bytes, of an individual vertex.
+ int element_size() const noexcept { return element_size_; }
+
+ // The GL identifier for this buffer.
+ unsigned int id() const noexcept { return buffer_.id(); }
+
+ private:
+ Buffer buffer_;
+ int element_size_;
+};
+
+// The types that can go in an element buffer.
+template <typename T>
+concept IsElement = std::same_as<T, uint8_t> || std::same_as<T, uint16_t> ||
+ std::same_as<T, uint32_t>;
+
+// An element buffer.
+class ElementBuffer final {
+ public:
+ explicit ElementBuffer() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) =
+ default;
+
+ // Constructs an element buffer containing the specified data.
+ template <IsElement T>
+ explicit ElementBuffer(absl::Span<const T> data,
+ Buffer::AccessFrequency frequency,
+ Buffer::AccessNature nature) {
+ SetData(data, frequency, nature);
+ }
+
+ ElementBuffer(const ElementBuffer&) = default;
+ ElementBuffer& operator=(const ElementBuffer&) = default;
+ ElementBuffer(ElementBuffer&&) noexcept = default;
+ ElementBuffer& operator=(ElementBuffer&&) noexcept = default;
+
+ // Loads data into the buffer.
+ template <IsElement T>
+ void SetData(absl::Span<const T> data, Buffer::AccessFrequency frequency,
+ Buffer::AccessNature nature) {
+ buffer_.SetData(data.data(), data.size() * sizeof(T), frequency, nature);
+ element_size_ = sizeof(T);
+ }
+
+ // The number of elements in this buffer.
+ int size() const noexcept {
+ assert(buffer_.size() % element_size() == 0);
+ return buffer_.size() / element_size();
+ }
+
+ // The size, in bytes, of an individual element. This will always be 1, 2,
+ // or 4.
+ int element_size() const noexcept {
+ assert(element_size_ == 1 || element_size_ == 2 || element_size_ == 4);
+ return element_size_;
+ }
+
+ // The GL identifier for this buffer.
+ unsigned int id() const noexcept { return buffer_.id(); }
+
+ private:
+ Buffer buffer_;
+ int element_size_;
+};
+
+} // namespace gl
+
+#endif // GLPLANET_SRC_GL_BUFFER_H_
diff --git a/src/gl/draw.cc b/src/gl/draw.cc
new file mode 100644
index 0000000..9d20fe1
--- /dev/null
+++ b/src/gl/draw.cc
@@ -0,0 +1,56 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/gl/draw.h"
+
+#include <assert.h>
+
+#include "src/gl/error.h"
+#include "src/gl/vertex_array.h"
+#include "src/util.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+void DrawElements(const gl::VertexArray& vao, Primitive mode) {
+ gl_internal::CheckThreadSafety();
+
+ GLenum type;
+ switch (vao.element_buffer().element_size()) {
+ case 1:
+ type = GL_UNSIGNED_BYTE;
+ break;
+ case 2:
+ type = GL_UNSIGNED_SHORT;
+ break;
+ case 4:
+ type = GL_UNSIGNED_INT;
+ break;
+ default:
+ assert(false);
+ };
+
+ glDrawElements(FromEnum(mode), vao.element_buffer().size(), type,
+ /*indices=*/0);
+ // This error check is necessary because the mode could be unsupported.
+ gl_internal::ErrorCheck();
+}
+
+void SetViewport(int x, int y, int width, int height) {
+ gl_internal::CheckThreadSafety();
+ glViewport(x, y, width, height);
+ gl_internal::ErrorCheck();
+}
+
+} // namespace gl
diff --git a/src/gl/draw.h b/src/gl/draw.h
new file mode 100644
index 0000000..13deb43
--- /dev/null
+++ b/src/gl/draw.h
@@ -0,0 +1,217 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Drawing to the framebuffer.
+
+#ifndef GLPLANET_SRC_GL_DRAW_H_
+#define GLPLANET_SRC_GL_DRAW_H_
+
+#include <Eigen/Core>
+#include <initializer_list>
+#include <stdexcept>
+
+#include "src/gl/buffer.h"
+#include "src/gl/error.h"
+#include "src/gl/shader.h"
+#include "src/gl/texture.h"
+#include "src/gl/vertex_array.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+// Makes the specified shader program active.
+inline void SetActiveShaderProgram(ShaderProgram& program) {
+ gl_internal::CheckThreadSafety();
+ glUseProgram(program.id());
+ // This error check is necessary because glUseProgram can fail.
+ gl_internal::ErrorCheck();
+}
+
+// RAII version of the above: While live, makes the specified shader program
+// active.
+class ActiveShaderProgram final {
+ public:
+ explicit ActiveShaderProgram(ShaderProgram& program) {
+ gl_internal::CheckThreadSafety();
+ glGetIntegerv(GL_CURRENT_PROGRAM, &old_);
+ gl_internal::UnnecessaryErrorCheck();
+ SetActiveShaderProgram(program);
+ }
+
+ ActiveShaderProgram(const ActiveShaderProgram&) = delete;
+ ActiveShaderProgram& operator=(const ActiveShaderProgram&) = delete;
+
+ ~ActiveShaderProgram() {
+ gl_internal::CheckThreadSafety();
+ glUseProgram(old_);
+ // This error check is necessary because glUseProgram can fail.
+ gl_internal::ErrorCheck();
+ }
+
+ private:
+ GLint old_;
+};
+
+// Sets the specified uniform in the active shader. Throws std::logic_error in
+// the event of a type error (e.g., you set a mat4 uniform to a single integer).
+inline void SetActiveShaderUniform(int index, int v0) {
+ gl_internal::CheckThreadSafety();
+ glUniform1i(index, v0);
+ // This error check is necessary because the shader variable might have a
+ // different type than was passed in.
+ gl_internal::ErrorCheck();
+}
+inline void SetActiveShaderUniform(int index, const Eigen::Vector2f& value) {
+ gl_internal::CheckThreadSafety();
+ glUniform2fv(index, /*count=*/1, value.data());
+ // This error check is necessary because the shader variable might have a
+ // different type than was passed in.
+ gl_internal::ErrorCheck();
+}
+inline void SetActiveShaderUniform(int index, const Eigen::Vector3f& value) {
+ gl_internal::CheckThreadSafety();
+ glUniform3fv(index, /*count=*/1, value.data());
+ // This error check is necessary because the shader variable might have a
+ // different type than was passed in.
+ gl_internal::ErrorCheck();
+}
+inline void SetActiveShaderUniform(int index, const Eigen::Matrix4f& value) {
+ gl_internal::CheckThreadSafety();
+ glUniformMatrix4fv(index, /*count=*/1, /*transpose=*/GL_FALSE, value.data());
+ // This error check is necessary because the shader variable might have a
+ // different type than was passed in.
+ gl_internal::ErrorCheck();
+}
+
+// Binds the specified VAO.
+inline void BindVertexArray(const VertexArray& vao) noexcept(
+ !(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glBindVertexArray(vao.id());
+ gl_internal::UnnecessaryErrorCheck();
+}
+
+// RAII version of the above: While live, binds the specified VAO.
+class BoundVertexArray final {
+ public:
+ explicit BoundVertexArray(const VertexArray& vao) noexcept(
+ !(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &old_);
+ gl_internal::UnnecessaryErrorCheck();
+ BindVertexArray(vao);
+ }
+
+ BoundVertexArray(const BoundVertexArray&) = delete;
+ BoundVertexArray& operator=(const BoundVertexArray&) = delete;
+
+ ~BoundVertexArray() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glBindVertexArray(old_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ private:
+ GLint old_;
+};
+
+// Sets the current texture unit.
+inline void UseTextureUnit(int unit) {
+ gl_internal::CheckThreadSafety();
+ glActiveTexture(GL_TEXTURE0 + unit);
+ // This error check is necessary because the user might have passed in a
+ // too-large index for the texture unit.
+ gl_internal::ErrorCheck();
+}
+
+// Binds the specified 2D texture.
+inline void BindTexture2d(const Texture2d& tex) noexcept(
+ !(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glBindTexture(GL_TEXTURE_2D, tex.id());
+ gl_internal::UnnecessaryErrorCheck();
+}
+
+// RAII version of the above: While live, binds the specified 2D texture.
+class BoundTexture2d final {
+ public:
+ explicit BoundTexture2d(const Texture2d& tex) noexcept(
+ !(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_);
+ gl_internal::UnnecessaryErrorCheck();
+ BindTexture2d(tex);
+ }
+
+ BoundTexture2d(const BoundTexture2d&) = delete;
+ BoundTexture2d& operator=(const BoundTexture2d&) = delete;
+
+ ~BoundTexture2d() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glBindTexture(GL_TEXTURE_2D, old_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ private:
+ GLint old_;
+};
+
+enum class GlBuffer : GLenum {
+ kColor = GL_COLOR_BUFFER_BIT,
+ kDepth = GL_DEPTH_BUFFER_BIT,
+ kStencil = GL_STENCIL_BUFFER_BIT,
+};
+
+inline void Clear(const std::initializer_list<GlBuffer>& buffers) noexcept(
+ !(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+
+ GLenum mask = 0;
+ for (GlBuffer b : buffers) {
+ mask |= FromEnum(b);
+ }
+ glClear(mask);
+ gl_internal::UnnecessaryErrorCheck();
+}
+
+enum class Primitive : GLenum {
+ kPoints = GL_POINTS,
+ kLineStrip = GL_LINE_STRIP,
+ kLineLoop = GL_LINE_LOOP,
+ kLines = GL_LINES,
+ kLineStripAdjacency = GL_LINE_STRIP_ADJACENCY,
+ kLinesAdjacency = GL_LINES_ADJACENCY,
+ kTriangleStrip = GL_TRIANGLE_STRIP,
+ kTriangleFan = GL_TRIANGLE_FAN,
+ kTriangles = GL_TRIANGLES,
+ kTriangleStripAdjacency = GL_TRIANGLE_STRIP_ADJACENCY,
+ kTrianglesAdjacency = GL_TRIANGLES_ADJACENCY,
+ kPatches = GL_PATCHES,
+};
+
+// Draws the elements from the specified VAO. The VAO must already be bound.
+void DrawElements(const gl::VertexArray&, Primitive);
+
+void SetViewport(int x, int y, int width, int height);
+
+} // namespace gl
+
+#endif // GLPLANET_SRC_GL_DRAW_H_
diff --git a/src/gl/error.cc b/src/gl/error.cc
new file mode 100644
index 0000000..20b2c74
--- /dev/null
+++ b/src/gl/error.cc
@@ -0,0 +1,84 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/gl/error.h"
+
+#include <new>
+#include <stdexcept>
+#include <thread>
+
+#include "third_party/glew/include/GL/glew.h"
+#include "third_party/abseil/absl/base/const_init.h"
+#include "third_party/abseil/absl/synchronization/mutex.h"
+#include "third_party/abseil/absl/types/optional.h"
+#include "third_party/abseil/absl/meta/type_traits.h"
+
+namespace gl_internal {
+
+#ifndef GLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS
+
+namespace {
+
+ABSL_CONST_INIT absl::Mutex thread_id_mu(absl::kConstInit);
+ABSL_CONST_INIT absl::optional<std::thread::id> thread_id;
+static_assert(absl::is_trivially_destructible<decltype(thread_id)>::value);
+
+} // namespace
+
+void RecordThreadImpl() {
+ absl::MutexLock lock(&thread_id_mu);
+ if (thread_id.has_value()) {
+ throw std::logic_error("GL: gl_internal::RecordThread called twice");
+ }
+ thread_id = std::this_thread::get_id();
+}
+
+void CheckThreadSafetyImpl() {
+ absl::MutexLock lock(&thread_id_mu);
+ if (!thread_id.has_value()) {
+ throw std::logic_error(
+ "GL: gl_internal::CheckThreadSafety called before "
+ "gl_internal::RecordThread");
+ }
+ if (std::this_thread::get_id() != *thread_id) {
+ throw std::logic_error("GL: detected access from multiple threads");
+ }
+}
+
+#endif // !defined(GLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS)
+
+void ErrorCheck() {
+ switch (glGetError()) {
+ case GL_NO_ERROR:
+ return;
+ case GL_INVALID_ENUM:
+ throw std::invalid_argument(
+ "GL: unacceptable value specified for enumerated argument");
+ case GL_INVALID_VALUE:
+ throw std::out_of_range("GL: numeric argument out of range");
+ case GL_INVALID_OPERATION:
+ throw std::logic_error(
+ "GL: specified operation not allowed in current state");
+ case GL_INVALID_FRAMEBUFFER_OPERATION:
+ throw std::logic_error("GL: framebuffer object is not complete");
+ case GL_OUT_OF_MEMORY:
+ throw std::bad_alloc();
+ case GL_STACK_UNDERFLOW:
+ throw std::runtime_error("GL: stack underflow");
+ case GL_STACK_OVERFLOW:
+ throw std::runtime_error("GL: stack overflow");
+ }
+}
+
+} // namespace gl_internal
diff --git a/src/gl/error.h b/src/gl/error.h
new file mode 100644
index 0000000..a56bae9
--- /dev/null
+++ b/src/gl/error.h
@@ -0,0 +1,68 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Error handling and thread-safety checking.
+
+#ifndef GLPLANET_SRC_GL_ERROR_H_
+#define GLPLANET_SRC_GL_ERROR_H_
+
+namespace gl_internal {
+
+#ifdef GLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS
+constexpr bool kThreadSafetyChecks = false;
+#else
+constexpr bool kThreadSafetyChecks = true;
+#endif
+
+#ifdef GLPLANET_DISABLE_AGGRESSIVE_ERROR_CHECKING
+constexpr bool kAggressiveErrorChecking = false;
+#else
+constexpr bool kAggressiveErrorChecking = true;
+#endif
+
+#ifndef GLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS
+
+void RecordThreadImpl();
+
+void CheckThreadSafetyImpl();
+
+#endif
+
+// Records the calling thread as the OpenGL thread.
+inline void RecordThread() noexcept(!kThreadSafetyChecks) {
+#ifndef GLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS
+ RecordThreadImpl();
+#endif
+}
+
+// Throws std::logic_error if the current thread is not the OpenGL thread.
+inline void CheckThreadSafety() noexcept(!kThreadSafetyChecks) {
+#ifndef GLPLANET_DISABLE_GL_THREAD_SAFETY_CHECKS
+ CheckThreadSafetyImpl();
+#endif
+}
+
+// Checks the GL state for errors and throws if an error is present.
+void ErrorCheck();
+
+// As ErrorCheck, but disabled by GLPLANET_DISABLE_AGGRESSIVE_ERROR_CHECKING.
+inline void UnnecessaryErrorCheck() noexcept(!kAggressiveErrorChecking) {
+ if constexpr (kAggressiveErrorChecking) {
+ ErrorCheck();
+ }
+}
+
+} // namespace gl_internal
+
+#endif // GLPLANET_SRC_GL_ERROR_H_
diff --git a/src/gl/init.cc b/src/gl/init.cc
new file mode 100644
index 0000000..b8ac83e
--- /dev/null
+++ b/src/gl/init.cc
@@ -0,0 +1,95 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/gl/init.h"
+
+#include <stdexcept>
+
+#include "src/gl/error.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/substitute.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+namespace {
+
+using ::gl_internal::ErrorCheck;
+using ::gl_internal::UnnecessaryErrorCheck;
+
+void InitializeGlew() {
+ glewExperimental = GL_TRUE;
+ if (GLenum err = glewInit(); err != GLEW_OK) {
+ const GLubyte* error_string = glewGetErrorString(err);
+ throw std::runtime_error(absl::StrCat(
+ "GL: glewInit failed: ", reinterpret_cast<const char*>(error_string)));
+ }
+}
+
+void CheckGlVersion() {
+ GLint major_version;
+ try {
+ glGetIntegerv(GL_MAJOR_VERSION, &major_version);
+ ErrorCheck();
+ } catch (const std::invalid_argument&) {
+ // GL_MAJOR_VERSION doesn't even exist yet, which means we're on OpenGL 2.
+ throw std::runtime_error("GL: OpenGL 3.2 is required");
+ }
+ if (major_version == 3) {
+ GLint minor_version;
+ glGetIntegerv(GL_MINOR_VERSION, &minor_version);
+ UnnecessaryErrorCheck();
+ if (minor_version < 2) {
+ throw std::runtime_error("GL: OpenGL 3.2 is required");
+ }
+ }
+}
+
+void CheckGlExtensions() {
+ if (!GLEW_ARB_texture_storage) {
+ throw std::runtime_error("GL: ARB_texture_storage is required");
+ }
+ if (!GLEW_ARB_direct_state_access) {
+ throw std::runtime_error("GL: ARB_direct_state_access is required");
+ }
+}
+
+} // namespace
+
+void InitializeForThisThread() {
+ gl_internal::RecordThread();
+ InitializeGlew();
+ ErrorCheck(); // just to make sure we're starting clean
+ CheckGlVersion();
+ CheckGlExtensions();
+
+ // Assume byte-aligned rows in images.
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ UnnecessaryErrorCheck();
+}
+
+void SetClearColor(float red, float green, float blue, float alpha) {
+ gl_internal::CheckThreadSafety();
+
+ if (red < 0 || red > 1 || green < 0 || green > 1 || blue < 0 || blue > 1 ||
+ alpha < 0 || alpha > 1) {
+ throw std::invalid_argument(absl::Substitute(
+ "GL: invalid clear color ($0, $1, $2, $3)", red, green, blue, alpha));
+ }
+
+ glClearColor(red, green, blue, alpha);
+ gl_internal::UnnecessaryErrorCheck();
+}
+
+} // namespace gl
diff --git a/src/gl/init.h b/src/gl/init.h
new file mode 100644
index 0000000..ad58ac6
--- /dev/null
+++ b/src/gl/init.h
@@ -0,0 +1,86 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// OpenGL initialization.
+
+#ifndef GLPLANET_SRC_GL_INIT_H_
+#define GLPLANET_SRC_GL_INIT_H_
+
+#include "src/gl/error.h"
+#include "src/util.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+// OpenGL capabilities
+enum class Capability : GLenum {
+ kBlend = GL_BLEND,
+ kClipDistance0 = GL_CLIP_DISTANCE0,
+ kClipDistance1 = GL_CLIP_DISTANCE1,
+ kClipDistance2 = GL_CLIP_DISTANCE2,
+ kClipDistance3 = GL_CLIP_DISTANCE3,
+ kClipDistance4 = GL_CLIP_DISTANCE4,
+ kClipDistance5 = GL_CLIP_DISTANCE5,
+ kColorLogicOp = GL_COLOR_LOGIC_OP,
+ kCullFace = GL_CULL_FACE,
+ kDepthClamp = GL_DEPTH_CLAMP,
+ kDepthTest = GL_DEPTH_TEST,
+ kDither = GL_DITHER,
+ kFramebufferSrgb = GL_FRAMEBUFFER_SRGB,
+ kLineSmooth = GL_LINE_SMOOTH,
+ kMultisample = GL_MULTISAMPLE,
+ kPolygonOffsetFill = GL_POLYGON_OFFSET_FILL,
+ kPolygonOffsetLine = GL_POLYGON_OFFSET_LINE,
+ kPolygonOffsetPoint = GL_POLYGON_OFFSET_POINT,
+ kPolygonSmooth = GL_POLYGON_SMOOTH,
+ kRasterizerDiscard = GL_RASTERIZER_DISCARD,
+ kSampleAlphaToCoverage = GL_SAMPLE_ALPHA_TO_COVERAGE,
+ kSampleAlphaToOne = GL_SAMPLE_ALPHA_TO_ONE,
+ kSampleCoverage = GL_SAMPLE_COVERAGE,
+ kSampleShading = GL_SAMPLE_SHADING,
+ kSampleMask = GL_SAMPLE_MASK,
+ kScissorTest = GL_SCISSOR_TEST,
+ kStencilTest = GL_STENCIL_TEST,
+ kProgramPointSize = GL_PROGRAM_POINT_SIZE,
+
+ // Added in GL 3.1
+ kPrimitiveRestart = GL_PRIMITIVE_RESTART,
+
+ // Added in GL 3.2
+ kTextureCubeMapSeamless = GL_TEXTURE_CUBE_MAP_SEAMLESS,
+};
+
+// Initializes the OpenGL state machine in the current thread. The current
+// thread must have an OpenGL context bound.
+//
+// You must call this function exactly once in your program. Once you do so, you
+// may not call any other function in this namespace from any other thread.
+void InitializeForThisThread();
+
+// Enables the specified OpenGL capability.
+inline void Enable(Capability cap) noexcept(
+ !(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glEnable(FromEnum(cap));
+ gl_internal::UnnecessaryErrorCheck();
+}
+
+// Sets the clear color for the active framebuffer. Color coordinates should be
+// in the range [0, 1].
+void SetClearColor(float red, float green, float blue, float alpha);
+
+} // namespace gl
+
+#endif // GLPLANET_SRC_GL_INIT_H_
diff --git a/src/gl/shader.cc b/src/gl/shader.cc
new file mode 100644
index 0000000..77887f2
--- /dev/null
+++ b/src/gl/shader.cc
@@ -0,0 +1,204 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/gl/shader.h"
+
+#include <stdexcept>
+#include <string>
+
+#include "src/gl/error.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+namespace {
+
+using ::gl_internal::CheckThreadSafety;
+using ::gl_internal::ErrorCheck;
+using ::gl_internal::UnnecessaryErrorCheck;
+
+std::string GetInfoLog(
+ void (*gl_get)(GLuint, GLenum, GLint*),
+ void (*gl_get_info_log)(GLuint, GLsizei, GLsizei*, GLchar*),
+ GLuint resource) noexcept(!gl_internal::kAggressiveErrorChecking) {
+ GLint log_length;
+ gl_get(resource, GL_INFO_LOG_LENGTH, &log_length);
+ UnnecessaryErrorCheck();
+ if (log_length == 0) {
+ return "";
+ }
+
+ std::string log(log_length, '\0');
+ gl_get_info_log(resource, log_length, /*length=*/nullptr, log.data());
+ UnnecessaryErrorCheck();
+ log.pop_back(); // pop the nullptr
+ return log;
+}
+
+} // namespace
+
+Shader::Shader(GLenum type) : source_set_(false), compile_attempted_(false) {
+ CheckThreadSafety();
+
+ shader_ = glCreateShader(type);
+ UnnecessaryErrorCheck();
+ if (shader_ == 0) {
+ throw std::runtime_error("GL: failed to create shader");
+ }
+}
+
+void Shader::SetSource(absl::string_view source) {
+ CheckThreadSafety();
+
+ std::array<const GLchar*, 1> source_ptrs = {source.data()};
+ if (source.size() > std::numeric_limits<GLint>::max()) {
+ throw std::invalid_argument(
+ "GL: shader source is too large for OpenGL to handle");
+ }
+ std::array<GLint, source_ptrs.size()> lengths = {
+ static_cast<GLint>(source.size())};
+ glShaderSource(shader_, source_ptrs.size(), source_ptrs.data(),
+ lengths.data());
+ UnnecessaryErrorCheck();
+
+ source_set_ = true;
+ compile_attempted_ = false;
+}
+
+void Shader::Compile() {
+ CheckThreadSafety();
+
+ if (!source_set_) {
+ throw std::logic_error(
+ "GL: shader compilation requested, but no source available");
+ }
+
+ glCompileShader(shader_);
+ UnnecessaryErrorCheck();
+
+ compile_attempted_ = true;
+ compile_log_ = GetInfoLog(glGetShaderiv, glGetShaderInfoLog, shader_);
+
+ GLint compiled;
+ glGetShaderiv(shader_, GL_COMPILE_STATUS, &compiled);
+ UnnecessaryErrorCheck();
+ if (!compiled) {
+ throw std::runtime_error(
+ absl::StrCat("GL: failed to compile shader: ", compile_log_));
+ }
+}
+
+const std::string& Shader::compile_log() const {
+ if (!compile_attempted_) {
+ throw std::logic_error(
+ "GL: compilation log examined before shader was compiled");
+ }
+ return compile_log_;
+}
+
+ShaderProgram::ShaderProgram()
+ : has_fragment_shader_(false), link_attempted_(false) {
+ CheckThreadSafety();
+ program_ = glCreateProgram();
+ if (program_ == 0) {
+ throw std::runtime_error("GL: failed to create shader program");
+ }
+}
+
+void ShaderProgram::SetFragmentDataLocation(const char* name,
+ int framebuffer_id) {
+ CheckThreadSafety();
+
+ if (name == nullptr) {
+ throw std::invalid_argument(
+ "GL: requested fragment shader uniform with null name");
+ }
+ if (!has_fragment_shader_) {
+ throw std::logic_error(absl::StrCat(
+ "GL: cannot set data location for fragment shader uniform ", name,
+ ": no fragment shader present"));
+ }
+
+ glBindFragDataLocation(program_, framebuffer_id, name);
+ // This error check is be necessary because the name might be reserved.
+ ErrorCheck();
+}
+
+void ShaderProgram::Link() {
+ CheckThreadSafety();
+
+ glLinkProgram(program_);
+ UnnecessaryErrorCheck();
+
+ link_attempted_ = true;
+ link_log_ = GetInfoLog(glGetProgramiv, glGetProgramInfoLog, program_);
+
+ GLint linked;
+ glGetProgramiv(program_, GL_LINK_STATUS, &linked);
+ UnnecessaryErrorCheck();
+ if (!linked) {
+ throw std::runtime_error(
+ absl::StrCat("GL: failed to link shader program: ", link_log_));
+ }
+}
+
+const std::string& ShaderProgram::link_log() const {
+ if (!link_attempted_) {
+ throw std::logic_error(
+ "GL: link log examined before shader program was linked");
+ }
+ return link_log_;
+}
+
+int ShaderProgram::active_vertex_attribute(const char* name) const {
+ CheckThreadSafety();
+
+ if (name == nullptr) {
+ throw std::invalid_argument(
+ "GL: requested active vertex attribute with null name");
+ }
+
+ int index = glGetAttribLocation(program_, name);
+ // This error check is necessary because the program might not have been
+ // linked yet.
+ ErrorCheck();
+
+ if (index == -1) {
+ throw std::invalid_argument(absl::StrCat("GL: requested vertex attribute ",
+ name, " is reserved or inactive"));
+ }
+ return index;
+}
+
+int ShaderProgram::active_uniform(const char* name) const {
+ CheckThreadSafety();
+
+ if (name == nullptr) {
+ throw std::invalid_argument("GL: requested active uniform with null name");
+ }
+
+ int index = glGetUniformLocation(program_, name);
+ // This error check is necessary because the program might not have been
+ // linked yet.
+ ErrorCheck();
+ if (index == -1) {
+ throw std::invalid_argument(absl::StrCat("GL: requested uniform ", name,
+ " is reserved or inactive"));
+ }
+ return index;
+}
+
+} // namespace gl
diff --git a/src/gl/shader.h b/src/gl/shader.h
new file mode 100644
index 0000000..b4cfb63
--- /dev/null
+++ b/src/gl/shader.h
@@ -0,0 +1,211 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Shaders and shader programs.
+
+#ifndef GLPLANET_SRC_GL_SHADER_H_
+#define GLPLANET_SRC_GL_SHADER_H_
+
+#include <array>
+#include <string>
+
+#include "src/gl/error.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+class FragmentShader;
+class VertexShader;
+
+// An individual shader.
+//
+// The shader life cycle looks like this:
+//
+// Shader s;
+// s.SetSource(source_location);
+// s.Compile();
+// std::cerr << s.compile_log();
+//
+// The Compile function throws if errors occur during compilation, but warnings
+// are merely saved. You must retrieve the compilation log if you wish to report
+// warnings.
+class Shader {
+ public:
+ Shader(Shader&&) noexcept = default;
+ Shader& operator=(Shader&&) noexcept = default;
+
+ virtual ~Shader() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glDeleteShader(shader_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ // Loads the shader source. This function copies the source buffer, so the
+ // source buffer need not stay valid after this call.
+ //
+ // You may call this function repeatedly. Doing so invalidates the shader, and
+ // you must recompile it with Compile before using it.
+ void SetSource(absl::string_view source);
+
+ // Compiles the shader. Throws a std::runtime_error including the compilation
+ // log if compilation fails. If compilation succeeds but generates warnings,
+ // this function silently returns; check the compile_log member to see what
+ // the warnings were.
+ //
+ // You must call SetSource before calling this function.
+ //
+ // You may call this function repeatedly.
+ void Compile();
+
+ // Fetches the shader compilation log. If this is empty, there were no
+ // warnings during the compilation.
+ //
+ // You must call Compile before calling this function.
+ //
+ // You may call this function repeatedly.
+ const std::string& compile_log() const;
+
+ // The GL identifier for this shader.
+ unsigned int id() const noexcept { return shader_; }
+
+ protected:
+ GLuint shader_;
+ bool source_set_;
+ bool compile_attempted_;
+ std::string compile_log_;
+
+ private:
+ friend class FragmentShader;
+ friend class VertexShader;
+
+ explicit Shader(GLenum type);
+};
+
+// A fragment shader.
+class FragmentShader final : public Shader {
+ public:
+ explicit FragmentShader() : Shader(GL_FRAGMENT_SHADER) {}
+};
+
+// A vertex shader.
+class VertexShader final : public Shader {
+ public:
+ explicit VertexShader() : Shader(GL_VERTEX_SHADER) {}
+};
+
+// A shader program consisting of multiple shaders.
+//
+// The shader program life cycle looks like this:
+//
+// ShaderProgram p;
+// p.attach(s);
+// p.SetFragmentDataLocation("output_color", framebuffer_id);
+// p.Link();
+// std::cerr << s.link_log();
+// DoSomethingWith(p.active_vertex_attribute("whatever"));
+//
+// Analogously to Shader::Compile, Link throws if errors occur during linking,
+// but warnings are merely saved. You must retrieve the link log if you wish to
+// report warnings.
+class ShaderProgram final {
+ public:
+ explicit ShaderProgram();
+
+ ShaderProgram(ShaderProgram&&) noexcept = default;
+ ShaderProgram& operator=(ShaderProgram&&) noexcept = default;
+
+ ~ShaderProgram() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glDeleteProgram(program_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ // Attach shaders to the program. The shaders need not be compiled, but you
+ // must compile them before calling Link.
+ //
+ // You may call these functions repeatedly, so long as you never pass the same
+ // shader twice. In any case, calling these functions invalidates the program,
+ // and you must relink it with Link before using it.
+ void Attach(const VertexShader& shader) { AttachShader(shader); }
+ void Attach(const FragmentShader& shader) {
+ AttachShader(shader);
+ has_fragment_shader_ = true;
+ }
+
+ // Associates a fragment shader output variable with an output framebuffer.
+ // The variable name must not start with "gl_". The default framebuffer has ID
+ // 0.
+ //
+ // You must have a fragment shader attached to this program to call this
+ // function.
+ //
+ // You may call this function repeatedly. Doing so invalidates the program,
+ // and you must relink it with Link before using it.
+ void SetFragmentDataLocation(const char* name, int framebuffer_id);
+
+ // Links the shader program. Throws a std::runtime_error including the link
+ // log if linking fails. If linking succeeds but generates warnings, this
+ // function silently returns; check the link_log member to see what the
+ // warnings were.
+ //
+ // You may call this function repeatedly.
+ void Link();
+
+ // Fetches the program link log. If this is empty, there were no warnings
+ // during the link.
+ //
+ // You must call Link before calling this function.
+ //
+ // You may call this function repeatedly.
+ const std::string& link_log() const;
+
+ // The index of the specified active vertex attribute. The variable name must
+ // not start with "gl_".
+ //
+ // Throws std::invalid_argument if the specified name is reserved, inactive,
+ // or nonexistent. Throws std::logic_error if the program has not been linked.
+ int active_vertex_attribute(const char* name) const;
+
+ // The index of the specified active uniform variable. The variable name must
+ // not start with "gl_".
+ //
+ // Throws std::invalid_argument if the specified name is reserved, inactive,
+ // or nonexistent. Throws std::logic_error if the program has not been linked.
+ int active_uniform(const char* name) const;
+
+ // The GL identifier for this program.
+ unsigned int id() const noexcept { return program_; }
+
+ private:
+ void AttachShader(const Shader& shader) {
+ gl_internal::CheckThreadSafety();
+ glAttachShader(program_, shader.id());
+ // This error check is necessary because the user might have passed in the
+ // same shader twice.
+ gl_internal::ErrorCheck();
+ link_attempted_ = false;
+ }
+
+ GLuint program_;
+ bool has_fragment_shader_;
+ bool link_attempted_;
+ std::string link_log_;
+};
+
+} // namespace gl
+
+#endif // GLPLANET_SRC_GL_SHADER_H_
diff --git a/src/gl/texture.cc b/src/gl/texture.cc
new file mode 100644
index 0000000..cd877c9
--- /dev/null
+++ b/src/gl/texture.cc
@@ -0,0 +1,43 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/gl/texture.h"
+
+#include <stdexcept>
+
+#include "src/gl/error.h"
+#include "src/util.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+void Texture2d::LoadSubimage(int width, int height, PixelFormat format,
+ PixelType type, const void* pixels, int level) {
+ gl_internal::CheckThreadSafety();
+
+ if (level >= levels_) {
+ throw std::invalid_argument(
+ absl::StrCat("GL: invalid texture subimage level $0; maximum is $1",
+ level, levels_ - 1));
+ }
+
+ glTextureSubImage2D(texture_, level, /*xoffset=*/0, /*yoffset=*/0, width,
+ height, FromEnum(format), FromEnum(type), pixels);
+ // This error check is necessary for a variety of reasons, including a
+ // type/format mismatch.
+ gl_internal::ErrorCheck();
+}
+
+} // namespace gl
diff --git a/src/gl/texture.h b/src/gl/texture.h
new file mode 100644
index 0000000..17541e0
--- /dev/null
+++ b/src/gl/texture.h
@@ -0,0 +1,259 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Texture objects.
+
+#ifndef GLPLANET_SRC_GL_TEXTURE_H_
+#define GLPLANET_SRC_GL_TEXTURE_H_
+
+#include "src/gl/error.h"
+#include "src/util.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+class Texture2d;
+
+// A generic texture. You can't instantiate this directly; instead, instantiate
+// one of its derived classes with an explicit dimensionality.
+class Texture {
+ public:
+ // Options for the texture's sample format.
+ enum class Format : GLenum {
+ kR8 = GL_R8,
+ kR8Snorm = GL_R8_SNORM,
+ kR16 = GL_R16,
+ kR16Snorm = GL_R16_SNORM,
+ kRg8 = GL_RG8,
+ kRg8Snorm = GL_RG8_SNORM,
+ kRg16 = GL_RG16,
+ kRg16Snorm = GL_RG16_SNORM,
+ kR3G3B2 = GL_R3_G3_B2,
+ kRgb4 = GL_RGB4,
+ kRgb5 = GL_RGB5,
+ kRgb8 = GL_RGB8,
+ kRgb8Snorm = GL_RGB8_SNORM,
+ kRgb10 = GL_RGB10,
+ kRgb12 = GL_RGB12,
+ kRgb16Snorm = GL_RGB16_SNORM,
+ kRgba2 = GL_RGBA2,
+ kRgba4 = GL_RGBA4,
+ kRgb5A1 = GL_RGB5_A1,
+ kRgba8 = GL_RGBA8,
+ kRgba8Snorm = GL_RGBA8_SNORM,
+ kRgb10A2 = GL_RGB10_A2,
+ kRgb10A2Ui = GL_RGB10_A2UI,
+ kRgba12 = GL_RGBA12,
+ kRgba16 = GL_RGBA16,
+ kSrgb8 = GL_SRGB8,
+ kSrgb8Alpha8 = GL_SRGB8_ALPHA8,
+ kR16f = GL_R16F,
+ kRg16f = GL_RG16F,
+ kRgb16f = GL_RGB16F,
+ kRgba16f = GL_RGBA16F,
+ kR32f = GL_R32F,
+ kRg32f = GL_RG32F,
+ kRgb32f = GL_RGB32F,
+ kRgba32f = GL_RGBA32F,
+ kR11fG11fB10f = GL_R11F_G11F_B10F,
+ kRgb9E5 = GL_RGB9_E5,
+ kR8i = GL_R8I,
+ kR8ui = GL_R8UI,
+ kR16i = GL_R16I,
+ kR16ui = GL_R16UI,
+ kR32i = GL_R32I,
+ kR32ui = GL_R32UI,
+ kRg8i = GL_RG8I,
+ kRg8ui = GL_RG8UI,
+ kRg16i = GL_RG16I,
+ kRg16ui = GL_RG16UI,
+ kRg32i = GL_RG32I,
+ kRg32ui = GL_RG32UI,
+ kRgb8i = GL_RGB8I,
+ kRgb8ui = GL_RGB8UI,
+ kRgb16i = GL_RGB16I,
+ kRgb16ui = GL_RGB16UI,
+ kRgb32i = GL_RGB32I,
+ kRgb32ui = GL_RGB32UI,
+ kRgba8i = GL_RGBA8I,
+ kRgba8ui = GL_RGBA8UI,
+ kRgba16i = GL_RGBA16I,
+ kRgba16ui = GL_RGBA16UI,
+ kRgba32i = GL_RGBA32I,
+ kRgba32ui = GL_RGBA32UI,
+ };
+
+ // Pixel order.
+ enum class PixelFormat : GLenum {
+ kRed = GL_RED,
+ kRg = GL_RG,
+ kRgb = GL_RGB,
+ kBgr = GL_BGR,
+ kRgba = GL_RGBA,
+ kBgra = GL_BGRA,
+ };
+
+ // Bits per pixel and subpixel.
+ enum class PixelType : GLenum {
+ kUnsignedByte = GL_UNSIGNED_BYTE,
+ kByte = GL_BYTE,
+ kUnsignedShort = GL_UNSIGNED_SHORT,
+ kShort = GL_SHORT,
+ kUnsignedInt = GL_UNSIGNED_INT,
+ kInt = GL_INT,
+ kFloat = GL_FLOAT,
+ kUnsignedByte332 = GL_UNSIGNED_BYTE_3_3_2,
+ kUnsignedByte233Rev = GL_UNSIGNED_BYTE_2_3_3_REV,
+ kUnsignedShort565 = GL_UNSIGNED_SHORT_5_6_5,
+ kUnsignedShort565Rev = GL_UNSIGNED_SHORT_5_6_5_REV,
+ kUnsignedShort4444 = GL_UNSIGNED_SHORT_4_4_4_4,
+ kUnsignedShort4444Rev = GL_UNSIGNED_SHORT_4_4_4_4_REV,
+ kUnsignedShort5551 = GL_UNSIGNED_SHORT_5_5_5_1,
+ kUnsignedShort1555Rev = GL_UNSIGNED_SHORT_1_5_5_5_REV,
+ kUnsignedInt8888 = GL_UNSIGNED_INT_8_8_8_8,
+ kUnsignedInt8888Rev = GL_UNSIGNED_INT_8_8_8_8_REV,
+ kUnsignedInt10x10x10x2 = GL_UNSIGNED_INT_10_10_10_2,
+ kUnsignedInt2x10x10x10Rev = GL_UNSIGNED_INT_2_10_10_10_REV,
+ };
+
+ // Filtering techniques.
+ enum class MinFilter : GLenum {
+ kNearest = GL_NEAREST,
+ kLinear = GL_LINEAR,
+ kNearestMipmapNearest = GL_NEAREST_MIPMAP_NEAREST,
+ kLinearMipmapNearest = GL_LINEAR_MIPMAP_NEAREST,
+ kNearestMipmapLinear = GL_NEAREST_MIPMAP_LINEAR,
+ kLinearMipmapLinear = GL_LINEAR_MIPMAP_LINEAR,
+ };
+ enum class MagFilter : GLenum {
+ kNearest = GL_NEAREST,
+ kLinear = GL_LINEAR,
+ };
+
+ // Wrapping techniques.
+ enum class Wrap : GLenum {
+ kClampToEdge = GL_CLAMP_TO_EDGE,
+ kClampToBorder = GL_CLAMP_TO_BORDER,
+ kMirroredRepeat = GL_MIRRORED_REPEAT,
+ kRepeat = GL_REPEAT,
+ kMirrorClampToEdge = GL_MIRROR_CLAMP_TO_EDGE,
+ };
+
+ Texture(Texture&&) noexcept = default;
+ Texture& operator=(Texture&&) noexcept = default;
+
+ virtual ~Texture() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glDeleteTextures(1, &texture_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ // The GL identifier for this texture.
+ unsigned int id() const noexcept { return texture_; }
+
+ protected:
+ int levels_;
+ GLuint texture_;
+
+ private:
+ friend class Texture2d;
+
+ explicit Texture(int levels) noexcept(!gl_internal::kThreadSafetyChecks)
+ : levels_(levels) {
+ gl_internal::CheckThreadSafety();
+ }
+};
+
+// A two-dimensional texture.
+class Texture2d final : public Texture {
+ public:
+ enum class Dimension : GLenum {
+ kS = GL_TEXTURE_WRAP_S,
+ kT = GL_TEXTURE_WRAP_T,
+ };
+
+ // Creates a two-dimensional texture with the specified pixel format, width,
+ // and height. The number of levels is equal to the number of mipmap layers
+ // for this texture; if you're not using mipmaps, you can leave it as 1.
+ explicit Texture2d(Format format, int width, int height, int levels = 1)
+ : Texture(levels), width_(width), height_(height) {
+ glCreateTextures(GL_TEXTURE_2D, 1, &texture_);
+ gl_internal::UnnecessaryErrorCheck();
+
+ glTextureStorage2D(texture_, levels, FromEnum(format), width_, height_);
+ // This error check is necessary because `format` could have been passed as
+ // an invalid GLenum.
+ gl_internal::ErrorCheck();
+ }
+
+ int width() const noexcept { return width_; }
+ int height() const noexcept { return height_; }
+
+ // Copies data from the specified buffer into GPU memory and associates it
+ // with this texture. width, height, format, and type describe the formatting
+ // of the data buffer, not this texture!
+ //
+ // There are no alignment restrictions on the buffer. It should consist of
+ // densely packed rows of pixels, left to right and top to bottom.
+ //
+ // As in the constructor, the optional level argument specifies which mipmap
+ // layer to populate. If you're not using mipmaps, you can leave it as 0.
+ void LoadSubimage(int width, int height, PixelFormat format, PixelType type,
+ const void* pixels, int level = 0);
+
+ // Fills all texture layers except layer 0 with automatically generated
+ // mipmaps. These mipmaps will generally be of lower quality than you would
+ // get if you generated them manually and loaded them with LoadSubimage.
+ // However, using them will yield a higher quality render than not using
+ // mipmaps at all.
+ void GenerateMipmaps() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glGenerateTextureMipmap(texture_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ void SetWrap(Dimension dimension, Wrap wrap) {
+ gl_internal::CheckThreadSafety();
+ glTexParameteri(GL_TEXTURE_2D, FromEnum(dimension), FromEnum(wrap));
+ // This error check is necessary because `dimension` or `wrap` could have
+ // been passed as an invalid GLenum.
+ gl_internal::ErrorCheck();
+ }
+
+ void SetMinFilter(MinFilter filter) {
+ gl_internal::CheckThreadSafety();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, FromEnum(filter));
+ // This error check is necessary because `filter` could have been passed as
+ // an invalid GLenum.
+ gl_internal::ErrorCheck();
+ }
+
+ void SetMagFilter(MagFilter filter) {
+ gl_internal::CheckThreadSafety();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, FromEnum(filter));
+ // This error check is necessary because `filter` could have been passed as
+ // an invalid GLenum.
+ gl_internal::ErrorCheck();
+ }
+
+ private:
+ int width_;
+ int height_;
+};
+
+} // namespace gl
+
+#endif // GLPLANET_SRC_GL_TEXTURE_H_
diff --git a/src/gl/vertex_array.cc b/src/gl/vertex_array.cc
new file mode 100644
index 0000000..067728e
--- /dev/null
+++ b/src/gl/vertex_array.cc
@@ -0,0 +1,73 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/gl/vertex_array.h"
+
+#include <stdint.h>
+
+#include "src/gl/error.h"
+#include "src/util.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+using ::gl_internal::CheckThreadSafety;
+using ::gl_internal::ErrorCheck;
+using ::gl_internal::UnnecessaryErrorCheck;
+
+void VertexArray::SetVertexAttributeFormat(int index, int size,
+ VertexAttributeType type, int offset,
+ bool normalized) {
+ CheckThreadSafety();
+
+ glEnableVertexArrayAttrib(array_, index);
+ // An error check is necessary here because the index might be too large for
+ // OpenGL to handle, but it can be folded into a future error check.
+ UnnecessaryErrorCheck();
+
+ switch (type) {
+ case VertexAttributeType::kHalfFloat:
+ case VertexAttributeType::kFloat:
+ glVertexArrayAttribFormat(array_, index, size, FromEnum(type), normalized,
+ offset);
+ break;
+ case VertexAttributeType::kByte:
+ case VertexAttributeType::kShort:
+ case VertexAttributeType::kInt:
+ case VertexAttributeType::kFixed:
+ case VertexAttributeType::kUnsignedByte:
+ case VertexAttributeType::kUnsignedShort:
+ case VertexAttributeType::kUnsignedInt:
+ case VertexAttributeType::kInt2x10x10x10Rev:
+ case VertexAttributeType::kUnsignedInt2x10x10x10Rev:
+ case VertexAttributeType::kUnsignedInt10f11f11fRev:
+ glVertexArrayAttribIFormat(array_, index, size, FromEnum(type), offset);
+ break;
+ case VertexAttributeType::kDouble:
+ glVertexArrayAttribLFormat(array_, index, size, FromEnum(type), offset);
+ break;
+ default:
+ throw std::invalid_argument(
+ absl::StrCat("GL: invalid VertexAttributeType ", type));
+ }
+ // This error check is necessary because a wide variety of argument
+ // combinations are invalid.
+ ErrorCheck();
+
+ glVertexArrayAttribBinding(array_, index, /*bindingindex=*/0);
+ UnnecessaryErrorCheck();
+}
+
+} // namespace gl
diff --git a/src/gl/vertex_array.h b/src/gl/vertex_array.h
new file mode 100644
index 0000000..5799c60
--- /dev/null
+++ b/src/gl/vertex_array.h
@@ -0,0 +1,108 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Vertex array objects.
+
+#ifndef GLPLANET_SRC_GL_VERTEX_ARRAY_H_
+#define GLPLANET_SRC_GL_VERTEX_ARRAY_H_
+
+#include <stdint.h>
+
+#include <utility>
+
+#include "src/gl/buffer.h"
+#include "src/gl/error.h"
+#include "third_party/abseil/absl/types/optional.h"
+#include "third_party/glew/include/GL/glew.h"
+
+namespace gl {
+
+enum class VertexAttributeType : GLenum {
+ kByte = GL_BYTE,
+ kShort = GL_SHORT,
+ kInt = GL_INT,
+ kFixed = GL_FIXED,
+ kFloat = GL_FLOAT,
+ kHalfFloat = GL_HALF_FLOAT,
+ kDouble = GL_DOUBLE,
+ kUnsignedByte = GL_UNSIGNED_BYTE,
+ kUnsignedShort = GL_UNSIGNED_SHORT,
+ kUnsignedInt = GL_UNSIGNED_INT,
+ kInt2x10x10x10Rev = GL_INT_2_10_10_10_REV,
+ kUnsignedInt2x10x10x10Rev = GL_UNSIGNED_INT_2_10_10_10_REV,
+ kUnsignedInt10f11f11fRev = GL_UNSIGNED_INT_10F_11F_11F_REV,
+};
+
+// A VAO.
+class VertexArray final {
+ public:
+ explicit VertexArray() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glCreateVertexArrays(1, &array_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ VertexArray(VertexArray&&) noexcept = default;
+ VertexArray& operator=(VertexArray&&) noexcept = default;
+
+ ~VertexArray() noexcept(!(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glDeleteVertexArrays(1, &array_);
+ gl_internal::UnnecessaryErrorCheck();
+ }
+
+ // Attaches the specified vertex buffer to the VAO.
+ void SetVertexBuffer(VertexBuffer vertices) {
+ gl_internal::CheckThreadSafety();
+ glVertexArrayVertexBuffer(array_, /*bindingindex=*/0, vertices.id(),
+ /*offset=*/0, vertices.element_size());
+ // This error check is necessary because the elements of the VBO might be
+ // too large for the implementation to handle.
+ gl_internal::ErrorCheck();
+ vertices_ = std::move(vertices);
+ }
+
+ // Specifies the organization of data in the attached VAO.
+ void SetVertexAttributeFormat(int index, int size, VertexAttributeType type,
+ int offset, bool normalized = false);
+
+ // Attaches the specified element buffer to the VAO. The element buffer must
+ // outlive this VAO.
+ void SetElementBuffer(ElementBuffer elements) noexcept(
+ !(gl_internal::kThreadSafetyChecks ||
+ gl_internal::kAggressiveErrorChecking)) {
+ gl_internal::CheckThreadSafety();
+ glVertexArrayElementBuffer(array_, elements.id());
+ gl_internal::UnnecessaryErrorCheck();
+ elements_ = std::move(elements);
+ }
+
+ // Returns the element buffer attached to this VAO.
+ const ElementBuffer& element_buffer() const noexcept { return elements_; }
+
+ // The GL identifier for this VAO.
+ unsigned int id() const noexcept { return array_; }
+
+ private:
+ GLuint array_;
+
+ VertexBuffer vertices_;
+ ElementBuffer elements_;
+};
+
+} // namespace gl
+
+#endif // GLPLANET_SRC_GL_VERTEX_ARRAY_H_
diff --git a/src/glplanet.cc b/src/glplanet.cc
new file mode 100644
index 0000000..69f4ac3
--- /dev/null
+++ b/src/glplanet.cc
@@ -0,0 +1,170 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/glplanet.h"
+
+#include <math.h>
+#include <stdlib.h>
+
+#include <chrono>
+#include <iostream>
+#include <stdexcept>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "res/res.h"
+#include "src/egl.h"
+#include "src/gl/draw.h"
+#include "src/gl/init.h"
+#include "src/scene.h"
+#include "src/x/connection.h"
+#include "src/x/event.h"
+#include "src/x/rpc.h"
+#include "src/x/screen.h"
+#include "src/x/types.h"
+#include "third_party/abseil/absl/meta/type_traits.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/types/span.h"
+#include "third_party/abseil/absl/types/variant.h"
+
+namespace glplanet {
+
+namespace {
+
+constexpr double kRadiansPerDegree = M_PI / 180.0;
+
+x::Connection ConnectToDisplaySpecifiedInEnvironment() {
+ const char* display_name = getenv("DISPLAY");
+ if (display_name == nullptr) {
+ throw std::runtime_error("DISPLAY unset");
+ }
+ return x::Connection(display_name);
+}
+
+} // namespace
+
+int Main(const MainOptions& options) {
+ x::Connection x = ConnectToDisplaySpecifiedInEnvironment();
+ egl::Display egl(x.AsXlibDisplay());
+
+ // Find an X visual that corresponds to our desired OpenGL settings.
+ egl::Configuration config =
+ egl.GetConfigurations(
+ {
+ {egl::kRedSize, 8},
+ {egl::kGreenSize, 8},
+ {egl::kBlueSize, 8},
+ {egl::kDepthSize, 1},
+ {egl::kRenderableTypeMask, egl::kOpenglBit},
+ {egl::kSampleBuffers, 1},
+ {egl::kSamples, 4},
+ },
+ /*limit=*/1)
+ .at(0);
+ x::Id visual = config.Get(egl::kNativeVisualId);
+
+ // Create the window we're going to display in, and tell X to map it. Set the
+ // window background to black so that if any newly exposed regions are briefly
+ // visible before being drawn on, the X server will fill them with the
+ // blackness of space.
+ std::vector<x::VoidCompletion> completions;
+ x::Screen screen = x.DefaultScreen();
+ x::Id window = x.GenerateId();
+ completions.push_back(x.CreateWindow({
+ .depth = screen.DepthOfVisual(visual),
+ .window = window,
+ .parent = screen.root(),
+ .x = 0,
+ .y = 0,
+ .width = 640,
+ .height = 480,
+ .border_width = 0,
+ .window_class = x::WindowClass::kInputOutput,
+ .visual_id = visual,
+ .background_pixel = screen.black_pixel(),
+ .event_mask = x::kExposure | x::kStructureNotify,
+ }));
+ completions.push_back(x.MapWindow(window));
+
+ // Create a rendering surface on the window and an OpenGL context to draw into
+ // that surface, and make that context current.
+ egl::Surface surface = egl.CreateWindowSurface(config, window);
+ egl::Context context = egl.CreateContext(config, egl::Api::kOpengl,
+ /*major=*/3,
+ /*minor=*/2);
+ egl::BindContext(surface, context);
+
+ gl::InitializeForThisThread();
+ gl::Enable(gl::Capability::kDepthTest);
+ gl::Enable(gl::Capability::kFramebufferSrgb);
+ // TODO(bbarenblat@gmail.com): Khronos says multisampling is turned on by
+ // default (see glEnable(3)). Do we actually need to turn it on explicitly?
+ gl::Enable(gl::Capability::kMultisample);
+ gl::SetClearColor(0.0, 0.0, 0.0, 1.0);
+
+ Scene scene({
+ .longitude_radians = options.longitude_degrees * kRadiansPerDegree,
+ .latitude_radians = options.latitude_degrees * kRadiansPerDegree,
+ });
+ scene.SetGlState();
+
+ // Ensure the window is mapped before we start drawing.
+ for (auto& completion : completions) {
+ std::move(completion).Check();
+ }
+
+ x::EventMonitor events(x);
+ int width = 640, height = 480;
+ while (true) {
+ // Handle any pending events.
+ int new_width = width, new_height = height;
+ while (absl::optional<x::Event> event = events.GetEventIfReady()) {
+ absl::visit(
+ [&](auto&& ev) {
+ using T = absl::decay_t<decltype(ev)>;
+ if constexpr (std::is_same_v<T, x::ConfigureNotifyEvent>) {
+ new_width = ev.width();
+ new_height = ev.height();
+ }
+ },
+ *event);
+ }
+
+ if (new_width != width || new_height != height) {
+ // Reset the viewport so the globe is centered and unsquashed.
+ int min_dimension = std::min(new_width, new_height);
+ int x_offset = (new_width - min_dimension) / 2;
+ int y_offset = (new_height - min_dimension) / 2;
+ gl::Clear({gl::GlBuffer::kColor});
+ gl::SetViewport(x_offset, y_offset, min_dimension, min_dimension);
+ width = new_width;
+ height = new_height;
+ }
+
+ gl::Clear({gl::GlBuffer::kDepth});
+ scene.Draw(std::chrono::system_clock::now());
+
+ // Wait for the frame to be displayed.
+ surface.SwapBuffers();
+
+ // TODO(bbarenblat@gmail.com): Actually get a target frame rate going based
+ // on the display size, rather than clamping to ~6 fps.
+ usleep(96'000);
+ }
+
+ return 0;
+}
+
+} // namespace glplanet
diff --git a/src/glplanet.h b/src/glplanet.h
new file mode 100644
index 0000000..edf7b21
--- /dev/null
+++ b/src/glplanet.h
@@ -0,0 +1,31 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_GLPLANET_H_
+#define GLPLANET_SRC_GLPLANET_H_
+
+namespace glplanet {
+
+struct MainOptions {
+ // The part of the Earth under the camera, in degrees east longitude and north
+ // latitude.
+ double longitude_degrees = 0.0;
+ double latitude_degrees = 0.0;
+};
+
+int Main(const MainOptions&);
+
+} // namespace glplanet
+
+#endif // GLPLANET_SRC_GLPLANET_H_
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..b1f1875
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,113 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <getopt.h>
+#include <locale.h>
+
+#include <iostream>
+
+#include "src/glplanet.h"
+#include "src/x/init.h"
+#include "third_party/abseil/absl/strings/numbers.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+
+namespace {
+
+constexpr absl::string_view kShortUsage = "Usage: glplanet [OPTION...]\n";
+
+constexpr absl::string_view kHelp = R"(
+Render a 3D globe lit in real time.
+
+Options:
+ --help display this help and exit
+ --longitude=DEGREES center globe on this longitude, in degrees east
+ --latitude=DEGREES center globe on this latitude, in degrees north
+ --version display version and license information, and exit
+)";
+
+constexpr absl::string_view kAskForHelp =
+ "Try 'glplanet --help' for more information.\n";
+
+constexpr absl::string_view kVersionInfo = R"(glplanet development build
+Copyright 2021, 2022 Benjamin Barenblat
+Licensed under the Apache License, Version 2.0
+
+glplanet incorporates Eigen, whose source is governed by the terms of the
+Mozilla Public License, Version 2.0. Eigen's source is available at
+https://deb.debian.org/debian/pool/main/e/eigen3/eigen3_3.3.9.orig.tar.bz2.
+)";
+
+enum {
+ kHelpLongOption = 128,
+ kLongitudeLongOption,
+ kLatitudeLongOption,
+ kVersionLongOption,
+};
+
+} // namespace
+
+int main(int argc, char* argv[]) {
+ setlocale(LC_ALL, "");
+ x::Initialize();
+
+ glplanet::MainOptions glplanet_options;
+
+ static option long_options[] = {
+ {"help", no_argument, nullptr, kHelpLongOption},
+ {"longitude", required_argument, nullptr, kLongitudeLongOption},
+ {"latitude", required_argument, nullptr, kLatitudeLongOption},
+ {"version", no_argument, nullptr, kVersionLongOption},
+ {nullptr, 0, nullptr, 0},
+ };
+ while (true) {
+ int c = getopt_long(argc, argv, "", long_options, /*longindex=*/nullptr);
+ if (c == -1) {
+ break;
+ }
+ switch (c) {
+ case kLongitudeLongOption:
+ if (!absl::SimpleAtod(optarg, &glplanet_options.longitude_degrees)) {
+ std::cerr << "glplanet: invalid longitude " << optarg << '\n';
+ return 1;
+ }
+ break;
+
+ case kLatitudeLongOption:
+ if (!absl::SimpleAtod(optarg, &glplanet_options.latitude_degrees)) {
+ std::cerr << "glplanet: invalid latitude " << optarg << '\n';
+ return 1;
+ }
+ break;
+
+ case kHelpLongOption:
+ std::cout << kShortUsage << kHelp;
+ return 0;
+ case kVersionLongOption:
+ std::cout << kVersionInfo;
+ return 0;
+ case '?':
+ std::cerr << kAskForHelp;
+ return 1;
+ default:
+ std::cerr << "Internal error; please report.\n";
+ return 1;
+ }
+ }
+ if (optind != argc) {
+ std::cerr << kShortUsage << kAskForHelp;
+ return 1;
+ }
+
+ return glplanet::Main(glplanet_options);
+}
diff --git a/src/mesh.cc b/src/mesh.cc
new file mode 100644
index 0000000..873e74b
--- /dev/null
+++ b/src/mesh.cc
@@ -0,0 +1,72 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/mesh.h"
+
+#include <math.h>
+
+#include <string>
+#include <vector>
+
+#include "src/util.h"
+#include "third_party/abseil/absl/strings/substitute.h"
+
+namespace glplanet {
+
+std::string UvSphere::Coordinates::DebugString() const noexcept {
+ return absl::Substitute(
+ "Coordinates{.x = $0, .y = $1, .z = $2, .u = $3, .v = $4}", x, y, z, u,
+ v);
+}
+
+UvSphere::UvSphere(int sectors_per_turn, int slices) noexcept {
+ DCHECK(sectors_per_turn >= 3);
+ DCHECK(slices >= 2);
+
+ int sectors_per_slice = sectors_per_turn + 1;
+
+ for (int j = 0; j <= slices; ++j) {
+ for (int i = 0; i < sectors_per_slice; ++i) {
+ double longitude = i * (2.0 * M_PI / sectors_per_turn) - M_PI;
+ double latitude = j * (M_PI / slices) - M_PI / 2.0;
+ vertices.push_back({
+ .x = static_cast<float>(cos(latitude) * cos(longitude)),
+ .y = static_cast<float>(cos(latitude) * sin(longitude)),
+ .z = sinf(latitude),
+
+ .u = static_cast<float>(i / static_cast<double>(sectors_per_turn)),
+ .v = static_cast<float>(1 - j / static_cast<double>(slices)),
+ });
+ }
+ }
+
+ // Generate triangle elements.
+ for (int j = 0; j < slices; ++j) {
+ for (int i = 0; i < sectors_per_turn; ++i) {
+ uint32_t ll = j * sectors_per_slice + i;
+ uint32_t lr = ll + 1;
+ uint32_t ul = ll + sectors_per_slice;
+ uint32_t ur = ul + 1;
+
+ DCHECK(ll < vertices.size());
+ DCHECK(lr < vertices.size());
+ DCHECK(ul < vertices.size());
+ DCHECK(ur < vertices.size());
+
+ elements.insert(elements.end(), {ll, ur, ul, lr, ur, ll});
+ }
+ }
+}
+
+} // namespace glplanet
diff --git a/src/mesh.h b/src/mesh.h
new file mode 100644
index 0000000..1363eb1
--- /dev/null
+++ b/src/mesh.h
@@ -0,0 +1,80 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_MESH_H_
+#define GLPLANET_SRC_MESH_H_
+
+#include <stdint.h>
+
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include "third_party/abseil/absl/base/attributes.h"
+
+namespace glplanet {
+
+// A UV-mapped sphere.
+struct UvSphere {
+ // A point on the sphere--a vector of 5-tuples (x, y, z, u, v).
+ //
+ // - (x, y, z) are the Cartesian coordinates of each vertex in right-handed
+ // model space--that is, with x forward, y to the right, and z upward.
+ //
+ // - (u, v) are texture coordinates, with (0, 0) corresponding to the north
+ // pole at 180 degrees of longitude. (This matches the way textures are
+ // loaded into memory from an image in plate carree projection.) The u
+ // coordinates are not in the range [0, 1], so you will need to set your
+ // texture to GL_REPEAT in the s direction.
+ struct Coordinates {
+ float x ABSL_ATTRIBUTE_PACKED;
+ float y ABSL_ATTRIBUTE_PACKED;
+ float z ABSL_ATTRIBUTE_PACKED;
+
+ float u ABSL_ATTRIBUTE_PACKED;
+ float v ABSL_ATTRIBUTE_PACKED;
+
+ std::string DebugString() const noexcept;
+ };
+
+ // Creates a UV-mapped unit sphere. sectors_per_turn specifies the number of
+ // latitude steps per 360-degree turn; slices specifies the number of vertical
+ // slices of the sphere. These must both be at least 2; nothing less makes any
+ // sense.
+ //
+ // The sphere is oriented such that the prime meridian points in the +x
+ // direction.
+ UvSphere(int sectors_per_turn, int slices) noexcept;
+
+ // All points on the sphere.
+ //
+ // There are actually sectors_per_slice+1 sectors in each vertical slice of
+ // the sphere. The extra column of vertices is necessary to ensure the last
+ // stack has correct texture mapping. You will need to set your texture to
+ // GL_REPEAT in the s direction for this to work properly.
+ std::vector<Coordinates> vertices;
+
+ // The sphere's element array buffer. These elements should be drawn with
+ // GL_TRIANGLES.
+ std::vector<uint32_t> elements;
+};
+
+inline std::ostream& operator<<(std::ostream& out,
+ const UvSphere::Coordinates& c) noexcept {
+ return out << c.DebugString();
+}
+
+} // namespace glplanet
+
+#endif // GLPLANET_SRC_MESH_H_
diff --git a/src/mesh_test.cc b/src/mesh_test.cc
new file mode 100644
index 0000000..fabc84f
--- /dev/null
+++ b/src/mesh_test.cc
@@ -0,0 +1,136 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/mesh.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <stdint.h>
+
+#include <ostream>
+#include <string>
+
+#include "src/util.h"
+#include "third_party/abseil/absl/strings/substitute.h"
+
+namespace glplanet {
+namespace {
+
+using Coordinates = ::glplanet::UvSphere::Coordinates;
+using ::testing::ElementsAre;
+using ::testing::ExplainMatchResult;
+using ::testing::Field;
+using ::testing::FloatNear;
+using ::testing::UnorderedElementsAre;
+
+struct Triangle {
+ uint32_t a, b, c;
+
+ std::string DebugString() const noexcept {
+ return absl::Substitute("Triangle{$0, $1, $2}", a, b, c);
+ }
+};
+
+std::ostream& operator<<(std::ostream& out, const Triangle& t) noexcept {
+ return out << t.DebugString();
+}
+
+std::vector<Triangle> TrianglesIn(const std::vector<uint32_t>& elements) {
+ DCHECK(elements.size() % 3 == 0);
+ std::vector<Triangle> result;
+ for (int i = 0; i < elements.size(); i += 3) {
+ result.push_back(Triangle{elements[i], elements[i + 1], elements[i + 2]});
+ }
+ return result;
+}
+
+MATCHER_P5(CoordinatesAre, xv, yv, zv, uv, vv,
+ absl::Substitute("is an object whose fields (x, y, z, u, v) are "
+ "approximately ($0, $1, $2, $3, $4)",
+ xv, yv, zv, uv, vv)) {
+ return ExplainMatchResult(
+ AllOf(Field("x", &Coordinates::x, FloatNear(xv, 1e-15)),
+ Field("y", &Coordinates::y, FloatNear(yv, 1e-15)),
+ Field("z", &Coordinates::z, FloatNear(zv, 1e-15)),
+ Field("u", &Coordinates::u, FloatNear(uv, 1e-15)),
+ Field("v", &Coordinates::v, FloatNear(vv, 1e-15))),
+ arg, result_listener);
+}
+
+MATCHER_P3(TriangleIs, av, bv, cv,
+ absl::Substitute("is the triangle ($0, $1, $2)", av, bv, cv)) {
+ return arg.a == av && arg.b == bv && arg.c == cv;
+}
+
+TEST(MeshTest, SmallVertices) {
+ // This test is overconstrained. In particular, we check the returned
+ // coordinates with ElementsAre, rather than UnorderedElementsAre, and the u
+ // coordinates are exact, rather than being normalized to [0, 1]. This allows
+ // us to verify that the u coordinates increase monotonically as we travel
+ // east around the sphere, which is important to prevent discontinuities in
+ // texture mapping.
+ UvSphere s(4, 2);
+ EXPECT_THAT(s.vertices, ElementsAre(CoordinatesAre(0, 0, -1, 0, 1), //
+ CoordinatesAre(0, 0, -1, 0.25, 1), //
+ CoordinatesAre(0, 0, -1, 0.5, 1), //
+ CoordinatesAre(0, 0, -1, 0.75, 1), //
+ CoordinatesAre(0, 0, -1, 1, 1),
+
+ CoordinatesAre(-1, 0, 0, 0, 0.5), //
+ CoordinatesAre(0, -1, 0, 0.25, 0.5), //
+ CoordinatesAre(1, 0, 0, 0.5, 0.5), //
+ CoordinatesAre(0, 1, 0, 0.75, 0.5), //
+ CoordinatesAre(-1, 0, 0, 1, 0.5),
+
+ CoordinatesAre(0, 0, 1, 0, 0), //
+ CoordinatesAre(0, 0, 1, 0.25, 0), //
+ CoordinatesAre(0, 0, 1, 0.5, 0), //
+ CoordinatesAre(0, 0, 1, 0.75, 0), //
+ CoordinatesAre(0, 0, 1, 1, 0)));
+}
+
+TEST(MeshTest, SmallElements) {
+ UvSphere s(4, 2);
+ ASSERT_THAT(s.vertices, ElementsAre(CoordinatesAre(0, 0, -1, 0, 1), //
+ CoordinatesAre(0, 0, -1, 0.25, 1), //
+ CoordinatesAre(0, 0, -1, 0.5, 1), //
+ CoordinatesAre(0, 0, -1, 0.75, 1), //
+ CoordinatesAre(0, 0, -1, 1, 1),
+
+ CoordinatesAre(-1, 0, 0, 0, 0.5), //
+ CoordinatesAre(0, -1, 0, 0.25, 0.5), //
+ CoordinatesAre(1, 0, 0, 0.5, 0.5), //
+ CoordinatesAre(0, 1, 0, 0.75, 0.5), //
+ CoordinatesAre(-1, 0, 0, 1, 0.5),
+
+ CoordinatesAre(0, 0, 1, 0, 0), //
+ CoordinatesAre(0, 0, 1, 0.25, 0), //
+ CoordinatesAre(0, 0, 1, 0.5, 0), //
+ CoordinatesAre(0, 0, 1, 0.75, 0), //
+ CoordinatesAre(0, 0, 1, 1, 0)));
+ EXPECT_THAT(
+ TrianglesIn(s.elements),
+ UnorderedElementsAre(TriangleIs(0, 6, 5), TriangleIs(1, 6, 0), //
+ TriangleIs(1, 7, 6), TriangleIs(2, 7, 1), //
+ TriangleIs(2, 8, 7), TriangleIs(3, 8, 2), //
+ TriangleIs(3, 9, 8), TriangleIs(4, 9, 3),
+
+ TriangleIs(5, 11, 10), TriangleIs(6, 11, 5), //
+ TriangleIs(6, 12, 11), TriangleIs(7, 12, 6), //
+ TriangleIs(7, 13, 12), TriangleIs(8, 13, 7), //
+ TriangleIs(8, 14, 13), TriangleIs(9, 14, 8)));
+}
+
+} // namespace
+} // namespace glplanet
diff --git a/src/mvp.maxima b/src/mvp.maxima
new file mode 100644
index 0000000..8ec7d35
--- /dev/null
+++ b/src/mvp.maxima
@@ -0,0 +1,56 @@
+/* Copyright 2022 Benjamin Barenblat
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License. */
+
+/* Computes the model-view-projection matrix for glplanet. Variables are as follows:
+
+ - beta is the desired latitude in radians.
+
+ - gamma is the desired longitude in radians _west_ of the prime meridian (the
+ opposite of the usual convention).
+
+ - d is the distance to the center of the Earth (i.e., the z-coordinate of the
+ translated Earth), in Earth radii. Since this transformation also switches
+ from a right-handed model coordinate system to a left-handed scene
+ coordinate system, this should always be a positive value.
+
+ - phix and phiy are the camera field of view, in radians, in the x and y
+ directions.
+
+ - zn and zf are the near and far clip planes, again in Earth radii. These
+ should both be positive, and zf should be more than zn. */
+
+/* Rotation around the model z axis (turning to longitude) */
+Z : matrix([cos(gamma), -sin(gamma), 0, 0],
+ [sin(gamma), cos(gamma), 0, 0],
+ [ 0, 0, 1, 0],
+ [ 0, 0, 0, 1])$
+
+/* Rotation around the model y axis (tilting to latitude) */
+Y : matrix([ cos(beta), 0, sin(beta), 0],
+ [ 0, 1, 0, 0],
+ [-sin(beta), 0, cos(beta), 0],
+ [ 0, 0, 0, 1])$
+
+/* Transformation from model coordinates to view coordinates */
+T : matrix([ 0, 1, 0, 0],
+ [ 0, 0, 1, 0],
+ [-1, 0, 0, z],
+ [ 0, 0, 0, 1])$
+
+/* Perspective transformation */
+P : matrix([1 / tan(phix / 2), 0, 0, 0 ],
+ [ 0, 1 / tan(phiy / 2), 0, 0 ],
+ [ 0, 0, (zn + zf) / (zf - zn), 2 * zn * zf / (zn - zf)],
+ [ 0, 0, 1, 0 ])$
+
+P . T . Y . Z;
diff --git a/src/scene.cc b/src/scene.cc
new file mode 100644
index 0000000..4f3c9a7
--- /dev/null
+++ b/src/scene.cc
@@ -0,0 +1,258 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/scene.h"
+
+#include <math.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <Eigen/Core>
+#include <algorithm>
+#include <chrono>
+#include <iostream>
+#include <vector>
+
+#include "res/res.h"
+#include "src/astro.h"
+#include "src/gl/buffer.h"
+#include "src/gl/draw.h"
+#include "src/gl/shader.h"
+#include "src/gl/texture.h"
+#include "src/mesh.h"
+#include "src/webp.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/types/span.h"
+#include "third_party/date/include/date/date.h"
+#include "third_party/date/include/date/tz.h"
+
+namespace glplanet {
+
+namespace {
+
+constexpr int kSectorsPerTurn = 64;
+constexpr int kSlices = 64;
+
+Eigen::Matrix4f ModelViewProjection(const double longitude_radians,
+ const double latitude_radians) noexcept {
+ // This is the precomputed result of the following transformations, applied in
+ // order:
+ //
+ // 1. rotation around the model z axis (turning to longitude)
+ //
+ // 2. rotation around the model y axis (tilting to latitude)
+ //
+ // 3. transformation from model coordinates to view coordinates
+ //
+ // 4. perspective transformation
+ //
+ // This was computed with Maxima using the src/mvp.maxima script; see
+ // documentation there for what the variables mean.
+
+ const double sin_beta = sin(latitude_radians);
+ const double cos_beta = cos(latitude_radians);
+
+ const double sin_gamma = sin(-longitude_radians);
+ const double cos_gamma = cos(-longitude_radians);
+
+ constexpr double z = 12.0;
+
+ // Compute the camera field of view. We would like this to be such that we can
+ // see the full Earth.
+ constexpr double kEarthRadius = 1.0;
+ constexpr double tan_half_phix = (1.05 * kEarthRadius) / z;
+ constexpr double tan_half_phiy = (1.05 * kEarthRadius) / z;
+
+ constexpr double zn = z - kEarthRadius;
+ constexpr double zf = z; // We can't see beyond the horizon.
+
+ const double sin_beta_over_tan_half_phiy = sin_beta / tan_half_phiy;
+ constexpr double zf_minus_zn = zf - zn;
+ constexpr double clip_factor = (zf + zn) / zf_minus_zn;
+ const double clip_factor_cos_beta = clip_factor * cos_beta;
+
+ Eigen::Matrix4f mvp;
+ mvp(0, 0) = sin_gamma / tan_half_phix;
+ mvp(0, 1) = cos_gamma / tan_half_phix;
+ mvp(0, 2) = 0;
+ mvp(0, 3) = 0;
+ mvp(1, 0) = -sin_beta_over_tan_half_phiy * cos_gamma;
+ mvp(1, 1) = sin_beta_over_tan_half_phiy * sin_gamma;
+ mvp(1, 2) = cos_beta / tan_half_phiy;
+ mvp(1, 3) = 0;
+ mvp(2, 0) = -clip_factor_cos_beta * cos_gamma;
+ mvp(2, 1) = clip_factor_cos_beta * sin_gamma;
+ mvp(2, 2) = -sin_beta * clip_factor;
+ mvp(2, 3) = z * clip_factor - 2 * zf * zn / zf_minus_zn;
+ mvp(3, 0) = -cos_beta * cos_gamma;
+ mvp(3, 1) = cos_beta * sin_gamma;
+ mvp(3, 2) = -sin_beta;
+ mvp(3, 3) = z;
+ return mvp;
+}
+
+void LoadMipmapsFromTableau(absl::Span<const uint8_t> webp, int max_width,
+ int max_height, int column,
+ gl::Texture2d& texture) {
+ int x = column * max_width;
+ if (x % 2 == 1) {
+ // libwebp can't crop from an odd-numbered pixel. By convention, we've
+ // pushed anything that would be on an odd boundary one pixel to the right
+ // or down.
+ ++x;
+ }
+
+ int y = 0;
+ int width = max_width;
+ int height = max_height;
+ int level = 0;
+ while (true) {
+ std::vector<uint8_t> mipmap = DecodeWebp(webp, x, y, width, height);
+ texture.LoadSubimage(width, height, gl::Texture::PixelFormat::kRgb,
+ gl::Texture::PixelType::kUnsignedByte, mipmap.data(),
+ level);
+
+ if (width == 1 && height == 1) {
+ // We just loaded the last mipmap.
+ break;
+ }
+
+ y += height;
+ if (y % 2 == 1) {
+ // libwebp can't crop from an odd-numbered pixel. See note above about
+ // shifting one to the right or down.
+ ++y;
+ }
+ width = std::max(width / 2, 1);
+ height = std::max(height / 2, 1);
+ ++level;
+ }
+}
+
+void LoadStandardTexture(absl::Span<const uint8_t> webp, int column,
+ gl::Texture2d& tex) {
+ assert(tex.width() == 1024);
+ assert(tex.height() == 512);
+ LoadMipmapsFromTableau(webp,
+ /*max_width=*/1024, /*max_height=*/512, column, tex);
+ tex.SetWrap(gl::Texture2d::Dimension::kS, gl::Texture::Wrap::kRepeat);
+ tex.SetWrap(gl::Texture2d::Dimension::kT, gl::Texture::Wrap::kClampToEdge);
+ tex.SetMinFilter(gl::Texture::MinFilter::kLinearMipmapLinear);
+ tex.SetMagFilter(gl::Texture::MagFilter::kLinear);
+}
+
+void ReportShaderWarnings(const char* description,
+ absl::string_view log) noexcept {
+ if (!log.empty()) {
+ std::cerr << "glplanet: while " << description << ":\n" << log;
+ }
+}
+
+} // namespace
+
+Scene::Scene(const Options& options)
+ : planet_month_(0),
+ planet_(gl::Texture::Format::kSrgb8, 1024, 512, 11),
+ clouds_(gl::Texture::Format::kSrgb8, 1024, 512, 11),
+ mvp_(ModelViewProjection(options.longitude_radians, options.latitude_radians)) {
+ LoadStandardTexture(glplanet_resources::CloudsWebp(), /*column=*/0, clouds_);
+ SetUpShaders();
+ LoadMesh();
+}
+
+void Scene::SetUpShaders() {
+ gl::VertexShader vertex_shader;
+ vertex_shader.SetSource(glplanet_resources::VertexGlsl());
+ vertex_shader.Compile();
+ ReportShaderWarnings("compiling vertex shader", vertex_shader.compile_log());
+ program_.Attach(vertex_shader);
+
+ gl::FragmentShader fragment_shader;
+ fragment_shader.SetSource(glplanet_resources::FragmentGlsl());
+ fragment_shader.Compile();
+ ReportShaderWarnings("compiling fragment shader",
+ fragment_shader.compile_log());
+ program_.Attach(fragment_shader);
+ program_.SetFragmentDataLocation("out_color", 0);
+
+ program_.Link();
+ ReportShaderWarnings("linking shader program", program_.link_log());
+ uniform_mvp_ = program_.active_uniform("mvp");
+ uniform_planet_ = program_.active_uniform("planet");
+ uniform_clouds_ = program_.active_uniform("clouds");
+ uniform_sun_direction_ = program_.active_uniform("sun_direction");
+}
+
+void Scene::LoadMesh() {
+ UvSphere mesh(kSectorsPerTurn, kSlices);
+
+ vao_.SetVertexBuffer(gl::VertexBuffer(absl::MakeConstSpan(mesh.vertices),
+ gl::Buffer::AccessFrequency::kStatic,
+ gl::Buffer::AccessNature::kDraw));
+ vao_.SetVertexAttributeFormat(
+ program_.active_vertex_attribute("cartesian_position"), 3,
+ gl::VertexAttributeType::kFloat, offsetof(UvSphere::Coordinates, x));
+ vao_.SetVertexAttributeFormat(
+ program_.active_vertex_attribute("vertex_texture_coordinate"), 2,
+ gl::VertexAttributeType::kFloat, offsetof(UvSphere::Coordinates, u));
+
+ vao_.SetElementBuffer(gl::ElementBuffer(absl::MakeConstSpan(mesh.elements),
+ gl::Buffer::AccessFrequency::kStatic,
+ gl::Buffer::AccessNature::kDraw));
+}
+
+void Scene::SetGlState() {
+ gl::SetActiveShaderProgram(program_);
+
+ gl::SetActiveShaderUniform(uniform_mvp_, mvp_);
+
+ // TODO(bbarenblat@gmail.com): Assert that we have enough fragment shader
+ // texture units (GL_MAX_TEXTURE_IMAGE_UNITS).
+
+ gl::UseTextureUnit(0);
+ gl::BindTexture2d(planet_);
+ gl::SetActiveShaderUniform(uniform_planet_, 0);
+
+ gl::UseTextureUnit(1);
+ gl::BindTexture2d(clouds_);
+ gl::SetActiveShaderUniform(uniform_clouds_, 1);
+
+ gl::BindVertexArray(vao_);
+}
+
+void Scene::Draw(std::chrono::system_clock::time_point now) {
+ if (date::month month =
+ date::year_month_day(std::chrono::floor<std::chrono::days>(now))
+ .month();
+ month != planet_month_) {
+ // The month has changed since we last drew. Reload the planet texture to
+ // reflect current snow levels.
+ LoadStandardTexture(glplanet_resources::EarthWebp(), unsigned{month} - 1,
+ planet_);
+ planet_month_ = month;
+ }
+
+ const auto& [noon_longitude, noon_latitude] =
+ HighNoonLocation(std::chrono::time_point_cast<std::chrono::milliseconds>(
+ date::clock_cast<date::tai_clock>(now)));
+ Eigen::Vector3f to_sun{
+ static_cast<float>(cos(noon_latitude) * cos(noon_longitude)),
+ static_cast<float>(cos(noon_latitude) * sin(noon_longitude)),
+ sinf(noon_latitude)};
+ gl::SetActiveShaderUniform(uniform_sun_direction_, to_sun);
+
+ gl::DrawElements(vao_, gl::Primitive::kTriangles);
+} // namespace glplanet
+
+} // namespace glplanet
diff --git a/src/scene.h b/src/scene.h
new file mode 100644
index 0000000..36da1e0
--- /dev/null
+++ b/src/scene.h
@@ -0,0 +1,87 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_SCENE_H_
+#define GLPLANET_SRC_SCENE_H_
+
+#include <stdint.h>
+
+#include <Eigen/Core>
+#include <chrono>
+
+#include "src/gl/buffer.h"
+#include "src/gl/draw.h"
+#include "src/gl/shader.h"
+#include "src/gl/texture.h"
+#include "src/gl/vertex_array.h"
+#include "third_party/abseil/absl/base/attributes.h"
+#include "third_party/date/include/date/date.h"
+#include "third_party/date/include/date/tz.h"
+
+namespace glplanet {
+
+// The scene to draw.
+//
+// This class hangs onto some OpenGL state, so it is thread-hostile.
+class Scene final {
+ public:
+ struct Options {
+ // The part of the Earth under the camera, in radians east longitude and
+ // north latitude.
+ double longitude_radians;
+ double latitude_radians;
+ };
+
+ // Constructs a scene. There must be an OpenGL context bound to the current
+ // thread.
+ explicit Scene(const Options&);
+
+ Scene(Scene&&) noexcept = default;
+ Scene& operator=(Scene&&) noexcept = default;
+
+ // Sets up OpenGL state (binding buffers etc.) such that the scene can be
+ // drawn.
+ void SetGlState();
+
+ // Actually draws the scene.
+ void Draw(std::chrono::system_clock::time_point);
+
+ private:
+ void LoadTextures();
+ void SetUpShaders();
+ void LoadMesh();
+
+ date::month planet_month_;
+ gl::Texture2d planet_;
+
+ gl::Texture2d clouds_;
+
+ gl::ShaderProgram program_;
+ int uniform_mvp_;
+ int uniform_planet_;
+ int uniform_clouds_;
+ int uniform_sun_direction_;
+
+ Eigen::Matrix4f mvp_;
+
+ gl::VertexArray vao_;
+
+ int width_;
+ int height_;
+ bool window_size_changed_;
+};
+
+} // namespace glplanet
+
+#endif // GLPLANET_SRC_SCENE_H_
diff --git a/src/undo_xlib_dot_h_namespace_pollution.h b/src/undo_xlib_dot_h_namespace_pollution.h
new file mode 100644
index 0000000..db215b5
--- /dev/null
+++ b/src/undo_xlib_dot_h_namespace_pollution.h
@@ -0,0 +1,553 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// X11/Xlib.h #defines a lot of macros. This header reexports the ones we care
+// about as inline functions or constants; it then deletes most of the macros to
+// avoid surprises from transitive inclusion.
+
+#ifndef GLPLANET_SRC_UNDO_XLIB_DOT_H_NAMESPACE_POLLUTION_H_
+#define GLPLANET_SRC_UNDO_XLIB_DOT_H_NAMESPACE_POLLUTION_H_
+
+#include <X11/Xlib.h>
+
+namespace xlib {
+
+// clang-format off
+
+inline int (DefaultScreen)(Display* display) noexcept {
+ return DefaultScreen(display);
+}
+
+// clang-format on
+
+} // namespace xlib
+
+// X11/X.h
+#undef None
+#undef ParentRelative
+#undef CopyFromParent
+#undef PointerWindow
+#undef InputFocus
+#undef PointerRoot
+#undef AnyPropertyType
+#undef AnyKey
+#undef AnyButton
+#undef AllTemporary
+#undef CurrentTime
+#undef NoSymbol
+#undef NoEventMask
+#undef KeyPressMask
+#undef KeyReleaseMask
+#undef ButtonPressMask
+#undef ButtonReleaseMask
+#undef EnterWindowMask
+#undef LeaveWindowMask
+#undef PointerMotionMask
+#undef PointerMotionHintMask
+#undef Button1MotionMask
+#undef Button2MotionMask
+#undef Button3MotionMask
+#undef Button4MotionMask
+#undef Button5MotionMask
+#undef ButtonMotionMask
+#undef KeymapStateMask
+#undef ExposureMask
+#undef VisibilityChangeMask
+#undef StructureNotifyMask
+#undef ResizeRedirectMask
+#undef SubstructureNotifyMask
+#undef SubstructureRedirectMask
+#undef FocusChangeMask
+#undef PropertyChangeMask
+#undef ColormapChangeMask
+#undef OwnerGrabButtonMask
+#undef KeyPress
+#undef KeyRelease
+#undef ButtonPress
+#undef ButtonRelease
+#undef MotionNotify
+#undef EnterNotify
+#undef LeaveNotify
+#undef FocusIn
+#undef FocusOut
+#undef KeymapNotify
+#undef Expose
+#undef GraphicsExpose
+#undef NoExpose
+#undef VisibilityNotify
+#undef CreateNotify
+#undef DestroyNotify
+#undef UnmapNotify
+#undef MapNotify
+#undef MapRequest
+#undef ReparentNotify
+#undef ConfigureNotify
+#undef ConfigureRequest
+#undef GravityNotify
+#undef ResizeRequest
+#undef CirculateNotify
+#undef CirculateRequest
+#undef PropertyNotify
+#undef SelectionClear
+#undef SelectionRequest
+#undef SelectionNotify
+#undef ColormapNotify
+#undef ClientMessage
+#undef MappingNotify
+#undef GenericEvent
+#undef LASTEvent
+#undef ShiftMask
+#undef LockMask
+#undef ControlMask
+#undef Mod1Mask
+#undef Mod2Mask
+#undef Mod3Mask
+#undef Mod4Mask
+#undef Mod5Mask
+#undef ShiftMapIndex
+#undef LockMapIndex
+#undef ControlMapIndex
+#undef Mod1MapIndex
+#undef Mod2MapIndex
+#undef Mod3MapIndex
+#undef Mod4MapIndex
+#undef Mod5MapIndex
+#undef Button1Mask
+#undef Button2Mask
+#undef Button3Mask
+#undef Button4Mask
+#undef Button5Mask
+#undef AnyModifier
+#undef Button1
+#undef Button2
+#undef Button3
+#undef Button4
+#undef Button5
+#undef NotifyNormal
+#undef NotifyGrab
+#undef NotifyUngrab
+#undef NotifyWhileGrabbed
+#undef NotifyHint
+#undef NotifyAncestor
+#undef NotifyVirtual
+#undef NotifyInferior
+#undef NotifyNonlinear
+#undef NotifyNonlinearVirtual
+#undef NotifyPointer
+#undef NotifyPointerRoot
+#undef NotifyDetailNone
+#undef VisibilityUnobscured
+#undef VisibilityPartiallyObscured
+#undef VisibilityFullyObscured
+#undef PlaceOnTop
+#undef PlaceOnBottom
+#undef FamilyInternet
+#undef FamilyDECnet
+#undef FamilyChaos
+#undef FamilyInternet6
+#undef FamilyServerInterpreted
+#undef PropertyNewValue
+#undef PropertyDelete
+#undef ColormapUninstalled
+#undef ColormapInstalled
+#undef GrabModeSync
+#undef GrabModeAsync
+#undef GrabSuccess
+#undef AlreadyGrabbed
+#undef GrabInvalidTime
+#undef GrabNotViewable
+#undef GrabFrozen
+#undef AsyncPointer
+#undef SyncPointer
+#undef ReplayPointer
+#undef AsyncKeyboard
+#undef SyncKeyboard
+#undef ReplayKeyboard
+#undef AsyncBoth
+#undef SyncBoth
+#undef RevertToNone
+#undef RevertToPointerRoot
+#undef RevertToParent
+#undef Success
+#undef BadRequest
+#undef BadValue
+#undef BadWindow
+#undef BadPixmap
+#undef BadAtom
+#undef BadCursor
+#undef BadFont
+#undef BadMatch
+#undef BadDrawable
+#undef BadAccess
+#undef BadAlloc
+#undef BadColor
+#undef BadGC
+#undef BadIDChoice
+#undef BadName
+#undef BadLength
+#undef BadImplementation
+#undef FirstExtensionError
+#undef LastExtensionError
+#undef InputOutput
+#undef InputOnly
+#undef CWBackPixmap
+#undef CWBackPixel
+#undef CWBorderPixmap
+#undef CWBorderPixel
+#undef CWBitGravity
+#undef CWWinGravity
+#undef CWBackingStore
+#undef CWBackingPlanes
+#undef CWBackingPixel
+#undef CWOverrideRedirect
+#undef CWSaveUnder
+#undef CWEventMask
+#undef CWDontPropagate
+#undef CWColormap
+#undef CWCursor
+#undef CWX
+#undef CWY
+#undef CWWidth
+#undef CWHeight
+#undef CWBorderWidth
+#undef CWSibling
+#undef CWStackMode
+#undef ForgetGravity
+#undef NorthWestGravity
+#undef NorthGravity
+#undef NorthEastGravity
+#undef WestGravity
+#undef CenterGravity
+#undef EastGravity
+#undef SouthWestGravity
+#undef SouthGravity
+#undef SouthEastGravity
+#undef StaticGravity
+#undef UnmapGravity
+#undef NotUseful
+#undef WhenMapped
+#undef Always
+#undef IsUnmapped
+#undef IsUnviewable
+#undef IsViewable
+#undef SetModeInsert
+#undef SetModeDelete
+#undef DestroyAll
+#undef RetainPermanent
+#undef RetainTemporary
+#undef Above
+#undef Below
+#undef TopIf
+#undef BottomIf
+#undef Opposite
+#undef RaiseLowest
+#undef LowerHighest
+#undef PropModeReplace
+#undef PropModePrepend
+#undef PropModeAppend
+#undef GXclear
+#undef GXand
+#undef GXandReverse
+#undef GXcopy
+#undef GXandInverted
+#undef GXnoop
+#undef GXxor
+#undef GXor
+#undef GXnor
+#undef GXequiv
+#undef GXinvert
+#undef GXorReverse
+#undef GXcopyInverted
+#undef GXorInverted
+#undef GXnand
+#undef GXset
+#undef LineSolid
+#undef LineOnOffDash
+#undef LineDoubleDash
+#undef CapNotLast
+#undef CapButt
+#undef CapRound
+#undef CapProjecting
+#undef JoinMiter
+#undef JoinRound
+#undef JoinBevel
+#undef FillSolid
+#undef FillTiled
+#undef FillStippled
+#undef FillOpaqueStippled
+#undef EvenOddRule
+#undef WindingRule
+#undef ClipByChildren
+#undef IncludeInferiors
+#undef Unsorted
+#undef YSorted
+#undef YXSorted
+#undef YXBanded
+#undef CoordModeOrigin
+#undef CoordModePrevious
+#undef Complex
+#undef Nonconvex
+#undef Convex
+#undef ArcChord
+#undef ArcPieSlice
+#undef GCFunction
+#undef GCPlaneMask
+#undef GCForeground
+#undef GCBackground
+#undef GCLineWidth
+#undef GCLineStyle
+#undef GCCapStyle
+#undef GCJoinStyle
+#undef GCFillStyle
+#undef GCFillRule
+#undef GCTile
+#undef GCStipple
+#undef GCTileStipXOrigin
+#undef GCTileStipYOrigin
+#undef GCFont
+#undef GCSubwindowMode
+#undef GCGraphicsExposures
+#undef GCClipXOrigin
+#undef GCClipYOrigin
+#undef GCClipMask
+#undef GCDashOffset
+#undef GCDashList
+#undef GCArcMode
+#undef GCLastBit
+#undef FontLeftToRight
+#undef FontRightToLeft
+#undef FontChange
+#undef XYBitmap
+#undef XYPixmap
+#undef ZPixmap
+#undef AllocNone
+#undef AllocAll
+#undef DoRed
+#undef DoGreen
+#undef DoBlue
+#undef CursorShape
+#undef TileShape
+#undef StippleShape
+#undef AutoRepeatModeOff
+#undef AutoRepeatModeOn
+#undef AutoRepeatModeDefault
+#undef LedModeOff
+#undef LedModeOn
+#undef KBKeyClickPercent
+#undef KBBellPercent
+#undef KBBellPitch
+#undef KBBellDuration
+#undef KBLed
+#undef KBLedMode
+#undef KBKey
+#undef KBAutoRepeatMode
+#undef MappingSuccess
+#undef MappingBusy
+#undef MappingFailed
+#undef MappingModifier
+#undef MappingKeyboard
+#undef MappingPointer
+#undef DontPreferBlanking
+#undef PreferBlanking
+#undef DefaultBlanking
+#undef DisableScreenSaver
+#undef DisableScreenInterval
+#undef DontAllowExposures
+#undef AllowExposures
+#undef DefaultExposures
+#undef ScreenSaverReset
+#undef ScreenSaverActive
+#undef HostInsert
+#undef HostDelete
+#undef EnableAccess
+#undef DisableAccess
+#undef StaticGray
+#undef GrayScale
+#undef StaticColor
+#undef PseudoColor
+#undef TrueColor
+#undef DirectColor
+#undef LSBFirst
+#undef MSBFirst
+
+// X11/Xfuncproto.h
+#undef NeedFunctionPrototypes
+#undef NeedVarargsPrototypes
+#undef NeedNestedPrototypes
+#undef NARROWPROTO
+#undef FUNCPROTO
+#undef NeedWidePrototypes
+
+// X11/Xosdefs.h
+#undef NULL_NOT_ZERO
+#undef _DARWIN_C_SOURCE
+#undef CSRG_BASED
+
+// X11/Xlib.h
+#undef XlibSpecificationRelease
+#undef Bool
+#undef Status
+#undef True
+#undef False
+#undef QueuedAlready
+#undef QueuedAfterReading
+#undef QueuedAfterFlush
+#undef ConnectionNumber
+#undef RootWindow
+#undef DefaultScreen
+#undef DefaultRootWindow
+#undef DefaultVisual
+#undef DefaultGC
+#undef BlackPixel
+#undef WhitePixel
+#undef AllPlanes
+#undef QLength
+#undef DisplayWidth
+#undef DisplayHeight
+#undef DisplayWidthMM
+#undef DisplayHeightMM
+#undef DisplayPlanes
+#undef DisplayCells
+#undef ScreenCount
+#undef ServerVendor
+#undef ProtocolVersion
+#undef ProtocolRevision
+#undef VendorRelease
+#undef DisplayString
+#undef DefaultDepth
+#undef DefaultColormap
+#undef BitmapUnit
+#undef BitmapBitOrder
+#undef BitmapPad
+#undef ImageByteOrder
+#undef NextRequest
+#undef LastKnownRequestProcessed
+#undef ScreenOfDisplay
+#undef DefaultScreenOfDisplay
+#undef DisplayOfScreen
+#undef RootWindowOfScreen
+#undef BlackPixelOfScreen
+#undef WhitePixelOfScreen
+#undef DefaultColormapOfScreen
+#undef DefaultDepthOfScreen
+#undef DefaultGCOfScreen
+#undef DefaultVisualOfScreen
+#undef WidthOfScreen
+#undef HeightOfScreen
+#undef WidthMMOfScreen
+#undef HeightMMOfScreen
+#undef PlanesOfScreen
+#undef CellsOfScreen
+#undef MinCmapsOfScreen
+#undef MaxCmapsOfScreen
+#undef DoesSaveUnders
+#undef DoesBackingStore
+#undef EventMaskOfScreen
+#undef XAllocID
+#undef XNRequiredCharSet
+#undef XNQueryOrientation
+#undef XNBaseFontName
+#undef XNOMAutomatic
+#undef XNMissingCharSet
+#undef XNDefaultString
+#undef XNOrientation
+#undef XNDirectionalDependentDrawing
+#undef XNContextualDrawing
+#undef XNFontInfo
+#undef XIMPreeditArea
+#undef XIMPreeditCallbacks
+#undef XIMPreeditPosition
+#undef XIMPreeditNothing
+#undef XIMPreeditNone
+#undef XIMStatusArea
+#undef XIMStatusCallbacks
+#undef XIMStatusNothing
+#undef XIMStatusNone
+#undef XNVaNestedList
+#undef XNQueryInputStyle
+#undef XNClientWindow
+#undef XNInputStyle
+#undef XNFocusWindow
+#undef XNResourceName
+#undef XNResourceClass
+#undef XNGeometryCallback
+#undef XNDestroyCallback
+#undef XNFilterEvents
+#undef XNPreeditStartCallback
+#undef XNPreeditDoneCallback
+#undef XNPreeditDrawCallback
+#undef XNPreeditCaretCallback
+#undef XNPreeditStateNotifyCallback
+#undef XNPreeditAttributes
+#undef XNStatusStartCallback
+#undef XNStatusDoneCallback
+#undef XNStatusDrawCallback
+#undef XNStatusAttributes
+#undef XNArea
+#undef XNAreaNeeded
+#undef XNSpotLocation
+#undef XNColormap
+#undef XNStdColormap
+#undef XNForeground
+#undef XNBackground
+#undef XNBackgroundPixmap
+#undef XNFontSet
+#undef XNLineSpace
+#undef XNCursor
+#undef XNQueryIMValuesList
+#undef XNQueryICValuesList
+#undef XNVisiblePosition
+#undef XNR6PreeditCallback
+#undef XNStringConversionCallback
+#undef XNStringConversion
+#undef XNResetState
+#undef XNHotKey
+#undef XNHotKeyState
+#undef XNPreeditState
+#undef XNSeparatorofNestedList
+#undef XBufferOverflow
+#undef XLookupNone
+#undef XLookupChars
+#undef XLookupKeySym
+#undef XLookupBoth
+#undef XIMReverse
+#undef XIMUnderline
+#undef XIMHighlight
+#undef XIMPrimary
+#undef XIMSecondary
+#undef XIMTertiary
+#undef XIMVisibleToForward
+#undef XIMVisibleToBackword
+#undef XIMVisibleToCenter
+#undef XIMPreeditUnKnown
+#undef XIMPreeditEnable
+#undef XIMPreeditDisable
+#undef XIMInitialState
+#undef XIMPreserveState
+#undef XIMStringConversionLeftEdge
+#undef XIMStringConversionRightEdge
+#undef XIMStringConversionTopEdge
+#undef XIMStringConversionBottomEdge
+#undef XIMStringConversionConcealed
+#undef XIMStringConversionWrapped
+#undef XIMStringConversionBuffer
+#undef XIMStringConversionLine
+#undef XIMStringConversionWord
+#undef XIMStringConversionChar
+#undef XIMStringConversionSubstitution
+#undef XIMStringConversionRetrieval
+#undef XIMHotKeyStateON
+#undef XIMHotKeyStateOFF
+
+#endif // GLPLANET_SRC_UNDO_XLIB_DOT_H_NAMESPACE_POLLUTION_H_
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..37d43da
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,35 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_UTIL_H_
+#define GLPLANET_SRC_UTIL_H_
+
+#include <assert.h>
+
+#include "third_party/abseil/absl/meta/type_traits.h"
+
+// A variant of assert that always evaluates its arguments, even when assertions
+// are disabled.
+#ifdef NDEBUG
+#define DCHECK(x) (void)(x)
+#else
+#define DCHECK(x) assert(x)
+#endif
+
+template <typename T>
+constexpr absl::underlying_type_t<T> FromEnum(T x) {
+ return static_cast<absl::underlying_type_t<T>>(x);
+}
+
+#endif // GLPLANET_SRC_GL_UTIL_H_
diff --git a/src/vertex.glsl b/src/vertex.glsl
new file mode 100644
index 0000000..edad07e
--- /dev/null
+++ b/src/vertex.glsl
@@ -0,0 +1,30 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#version 150
+
+in vec3 cartesian_position;
+in vec2 vertex_geographic_position;
+in vec2 vertex_texture_coordinate;
+
+uniform mat4 mvp;
+
+out vec3 model_cartesian_position;
+out vec2 texture_coordinate;
+
+void main() {
+ gl_Position = mvp * vec4(cartesian_position, 1.0);
+ model_cartesian_position = cartesian_position;
+ texture_coordinate = vertex_texture_coordinate;
+}
diff --git a/src/webp.cc b/src/webp.cc
new file mode 100644
index 0000000..f2741e6
--- /dev/null
+++ b/src/webp.cc
@@ -0,0 +1,80 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/webp.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <webp/decode.h>
+
+#include <new>
+#include <stdexcept>
+#include <vector>
+
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/types/span.h"
+
+namespace glplanet {
+
+std::vector<uint8_t> DecodeWebp(absl::Span<const uint8_t> webp, int x, int y,
+ int width, int height) {
+ size_t bytes = 3 * width * height;
+ std::vector<uint8_t> decoded(bytes, '\0');
+
+ WebPDecoderConfig config;
+ if (!WebPInitDecoderConfig(&config)) {
+ throw std::runtime_error("failed to initialize WebP decoder");
+ }
+
+ config.options.use_cropping = 1;
+ config.options.crop_left = x;
+ config.options.crop_top = y;
+ config.options.crop_width = width;
+ config.options.crop_height = height;
+
+ config.output.colorspace = MODE_RGB;
+ config.output.is_external_memory = 1;
+ config.output.u.RGBA.rgba = decoded.data();
+ config.output.u.RGBA.stride = 3 * width;
+ config.output.u.RGBA.size = bytes;
+
+ int error = WebPDecode(webp.data(), webp.size(), &config);
+ if (error != VP8_STATUS_OK) {
+ static constexpr absl::string_view error_prefix = "failed to decode WebP: ";
+ switch (error) {
+ case VP8_STATUS_OUT_OF_MEMORY:
+ throw std::bad_alloc();
+ case VP8_STATUS_INVALID_PARAM:
+ throw std::invalid_argument(
+ absl::StrCat(error_prefix, "invalid parameter"));
+ case VP8_STATUS_BITSTREAM_ERROR:
+ throw std::runtime_error(absl::StrCat(error_prefix, "bitstream error"));
+ case VP8_STATUS_UNSUPPORTED_FEATURE:
+ throw std::runtime_error(
+ absl::StrCat(error_prefix, "unsupported feature"));
+ case VP8_STATUS_SUSPENDED:
+ throw std::runtime_error(absl::StrCat(error_prefix, "suspended"));
+ case VP8_STATUS_USER_ABORT:
+ throw std::runtime_error(absl::StrCat(error_prefix, "aborted"));
+ case VP8_STATUS_NOT_ENOUGH_DATA:
+ throw std::runtime_error(absl::StrCat(error_prefix, "not enough data"));
+ default:
+ throw std::runtime_error(absl::StrCat(error_prefix, "unknown error"));
+ }
+ }
+ return decoded;
+}
+
+} // namespace glplanet
diff --git a/src/webp.h b/src/webp.h
new file mode 100644
index 0000000..0153501
--- /dev/null
+++ b/src/webp.h
@@ -0,0 +1,31 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_WEBP_H_
+#define GLPLANET_SRC_WEBP_H_
+
+#include <stdint.h>
+
+#include <vector>
+
+#include "third_party/abseil/absl/types/span.h"
+
+namespace glplanet {
+
+std::vector<uint8_t> DecodeWebp(absl::Span<const uint8_t> webp, int x, int y,
+ int width, int height);
+
+} // namespace glplanet
+
+#endif // GLPLANET_SRC_WEBP_H_
diff --git a/src/x/connection.cc b/src/x/connection.cc
new file mode 100644
index 0000000..f376a2a
--- /dev/null
+++ b/src/x/connection.cc
@@ -0,0 +1,174 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/x/connection.h"
+
+#include <X11/Xlib-xcb.h>
+#include <X11/Xlib.h>
+#include <stdint.h>
+#include <xcb/xcb.h>
+
+#include <array>
+#include <stdexcept>
+#include <utility>
+#include <vector>
+
+#include "src/undo_xlib_dot_h_namespace_pollution.h"
+//
+
+#include "src/util.h"
+#include "src/x/event.h"
+#include "src/x/util.h"
+#include "third_party/abseil/absl/cleanup/cleanup.h"
+#include "third_party/abseil/absl/container/btree_map.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/synchronization/mutex.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace x {
+
+namespace {
+
+std::pair<uint32_t, std::vector<uint32_t>> MarshalValueList(
+ const absl::btree_map<uint32_t, absl::optional<uint32_t>>&
+ values) noexcept {
+ uint32_t value_mask = 0;
+ std::vector<uint32_t> value_list;
+ for (auto& [k, v] : values) {
+ if (v.has_value()) {
+ value_mask |= k;
+ value_list.push_back(*v);
+ }
+ }
+ return {value_mask, value_list};
+}
+
+} // namespace
+
+Connection::Connection(const char* display_name) {
+ if (display_name == nullptr) {
+ throw std::invalid_argument("X: null display name");
+ }
+
+ xlib_ = XOpenDisplay(display_name);
+ if (xlib_ == nullptr) {
+ throw std::runtime_error("X: could not connect to X server");
+ }
+ XSetEventQueueOwner(xlib_, XCBOwnsEventQueue);
+
+ // We make a lot of calls into XCB, so cache the XCB handle.
+ xcb_ = XGetXCBConnection(xlib_);
+}
+
+VoidCompletion Connection::CreateWindow(
+ const CreateWindowOptions& options) noexcept {
+ const auto& [value_mask, value_list] =
+ MarshalValueList({{XCB_CW_BACK_PIXEL, options.background_pixel},
+ {XCB_CW_EVENT_MASK, options.event_mask}});
+ return VoidCompletion(
+ xcb_, xcb_create_window_checked(
+ xcb_, options.depth, options.window, options.parent, options.x,
+ options.y, options.width, options.height, options.border_width,
+ FromEnum(options.window_class), options.visual_id, value_mask,
+ value_list.data()));
+}
+
+VoidCompletion Connection::SendEvent(const Event& event, bool propagate,
+ Id destination_window,
+ const std::vector<EventMask>& event_mask) {
+ uint32_t serialized_event_mask = 0;
+ for (EventMask m : event_mask) {
+ serialized_event_mask |= FromEnum(m);
+ }
+ std::array<char, 32> serialized_event = SerializeEvent(event);
+ return VoidCompletion(
+ xcb_,
+ xcb_send_event_checked(xcb_, propagate, destination_window,
+ serialized_event_mask, serialized_event.data()));
+}
+
+EventMonitor::EventMonitor(Connection& x)
+ : x_(x), communication_window_(x_.GenerateId()) {
+ // Create a window for us to communicate with the watcher thread.
+ VoidCompletion create_window_completion =
+ x_.CreateWindow({.depth = 0, // as required for an InputOnly window
+ .window = communication_window_,
+ .parent = x_.DefaultScreen().root(),
+ .x = 0,
+ .y = 0,
+ .width = 1,
+ .height = 1,
+ .border_width = 0,
+ .window_class = WindowClass::kInputOnly,
+ .visual_id = XCB_COPY_FROM_PARENT});
+ InternAtomCompletion intern_atom_completion =
+ x_.InternOrGetAtomByName(kDoneAtomName);
+ std::move(create_window_completion).Check();
+ close_connection_atom_ = std::move(intern_atom_completion).Get();
+ watcher_thread_.emplace(&EventMonitor::WatcherThreadMain, this);
+}
+
+EventMonitor::~EventMonitor() {
+ // Send a client message through the connection so the waiter thread knows to
+ // exit.
+ xcb_client_message_event_t event = {.response_type = XCB_CLIENT_MESSAGE,
+ .format = 32,
+ .sequence = 0,
+ .window = communication_window_,
+ .type = close_connection_atom_,
+ .data = {}};
+ x_.SendEvent(ClientMessageEvent(event), /*propagate=*/false,
+ communication_window_, {EventMask::kNoEvent})
+ .Check();
+ watcher_thread_->join();
+ x_.DestroyWindow(communication_window_);
+}
+
+absl::optional<Event> EventMonitor::WaitForEventWithTimeout(
+ absl::Duration timeout) noexcept {
+ bool events_present = mu_.LockWhenWithTimeout(
+ absl::Condition(this, &EventMonitor::EventsPresent), timeout);
+ absl::Cleanup unlock = [&]() noexcept { mu_.Unlock(); };
+ if (events_present) {
+ Event event = std::move(pending_events_.front());
+ pending_events_.pop_front();
+ return event;
+ } else {
+ return absl::nullopt;
+ }
+}
+
+void EventMonitor::WatcherThreadMain() noexcept {
+ while (true) {
+ auto generic_event =
+ std::unique_ptr<xcb_generic_event_t, x_internal::FreeDeleter>(
+ xcb_wait_for_event(x_.AsXcbConnection()));
+ if (generic_event == nullptr) {
+ // The connection dropped or something.
+ return;
+ }
+
+ Event event = FromXcbGenericEvent(*generic_event);
+ if (const auto* client_message = absl::get_if<ClientMessageEvent>(&event);
+ client_message != nullptr &&
+ client_message->type() == close_connection_atom_) {
+ // Our destructor is running.
+ return;
+ }
+ absl::MutexLock lock(&mu_);
+ pending_events_.push_back(event);
+ }
+}
+
+} // namespace x
diff --git a/src/x/connection.h b/src/x/connection.h
new file mode 100644
index 0000000..83c6b18
--- /dev/null
+++ b/src/x/connection.h
@@ -0,0 +1,204 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// The main connection to the X server.
+
+#ifndef GLPLANET_SRC_X_CONNECTION_H_
+#define GLPLANET_SRC_X_CONNECTION_H_
+
+#include <X11/Xlib.h>
+#include <stdint.h>
+#include <xcb/xcb.h>
+#include <xcb/xcb_aux.h>
+
+#include <deque>
+#include <thread>
+#include <vector>
+
+#include "src/undo_xlib_dot_h_namespace_pollution.h"
+//
+
+#include "src/x/event.h"
+#include "src/x/rpc.h"
+#include "src/x/screen.h"
+#include "src/x/types.h"
+#include "third_party/abseil/absl/base/thread_annotations.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/synchronization/mutex.h"
+#include "third_party/abseil/absl/time/time.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace x {
+
+enum class WindowClass : uint16_t {
+ kCopyFromParent = XCB_WINDOW_CLASS_COPY_FROM_PARENT,
+ kInputOutput = XCB_WINDOW_CLASS_INPUT_OUTPUT,
+ kInputOnly = XCB_WINDOW_CLASS_INPUT_ONLY,
+};
+
+enum EventMask : uint32_t {
+ kNoEvent = XCB_EVENT_MASK_NO_EVENT,
+ kKeyPress = XCB_EVENT_MASK_KEY_PRESS,
+ kKeyRelease = XCB_EVENT_MASK_KEY_RELEASE,
+ kButtonPress = XCB_EVENT_MASK_BUTTON_PRESS,
+ kButtonRelease = XCB_EVENT_MASK_BUTTON_RELEASE,
+ kEnterWindow = XCB_EVENT_MASK_ENTER_WINDOW,
+ kLeaveWindow = XCB_EVENT_MASK_LEAVE_WINDOW,
+ kPointerMotion = XCB_EVENT_MASK_POINTER_MOTION,
+ kPointerMotionHint = XCB_EVENT_MASK_POINTER_MOTION_HINT,
+ kButton1Motion = XCB_EVENT_MASK_BUTTON_1_MOTION,
+ kButton2Motion = XCB_EVENT_MASK_BUTTON_2_MOTION,
+ kButton3Motion = XCB_EVENT_MASK_BUTTON_3_MOTION,
+ kButton4Motion = XCB_EVENT_MASK_BUTTON_4_MOTION,
+ kButton5Motion = XCB_EVENT_MASK_BUTTON_5_MOTION,
+ kButtonMotion = XCB_EVENT_MASK_BUTTON_MOTION,
+ kKeymapState = XCB_EVENT_MASK_KEYMAP_STATE,
+ kExposure = XCB_EVENT_MASK_EXPOSURE,
+ kVisibilityChange = XCB_EVENT_MASK_VISIBILITY_CHANGE,
+ kStructureNotify = XCB_EVENT_MASK_STRUCTURE_NOTIFY,
+ kResizeRedirect = XCB_EVENT_MASK_RESIZE_REDIRECT,
+ kSubstructureNotify = XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
+ kSubstructureRedirect = XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
+ kFocusChange = XCB_EVENT_MASK_FOCUS_CHANGE,
+ kPropertyChange = XCB_EVENT_MASK_PROPERTY_CHANGE,
+ kColorMapChange = XCB_EVENT_MASK_COLOR_MAP_CHANGE,
+ kOwnerGrabButton = XCB_EVENT_MASK_OWNER_GRAB_BUTTON,
+};
+
+// A connection to the X server.
+//
+// This class is thread-safe.
+class Connection final {
+ public:
+ struct CreateWindowOptions {
+ uint8_t depth;
+ Id window;
+ Id parent;
+ int16_t x;
+ int16_t y;
+ uint16_t width;
+ uint16_t height;
+ uint16_t border_width;
+ WindowClass window_class;
+ Id visual_id;
+
+ // TODO(bbarenblat@gmail.com): Support additional attributes.
+ absl::optional<Id> background_pixel = absl::nullopt;
+ absl::optional<uint32_t> event_mask = absl::nullopt;
+ };
+
+ explicit Connection(const char* display_name);
+
+ Connection(Connection&&) noexcept = default;
+ Connection& operator=(Connection&&) noexcept = default;
+
+ ~Connection() noexcept { XCloseDisplay(xlib_); }
+
+ Id GenerateId() noexcept { return xcb_generate_id(xcb_); }
+
+ Screen DefaultScreen() const noexcept {
+ return Screen(xcb_aux_get_screen(xcb_, xlib::DefaultScreen(xlib_)));
+ }
+
+ VoidCompletion CreateWindow(const CreateWindowOptions& options) noexcept;
+
+ VoidCompletion DestroyWindow(Id window) noexcept {
+ return VoidCompletion(xcb_, xcb_destroy_window_checked(xcb_, window));
+ }
+
+ VoidCompletion MapWindow(Id window) noexcept {
+ return VoidCompletion(xcb_, xcb_map_window_checked(xcb_, window));
+ }
+
+ // Per the X specification, name should be encoded as ISO 8859-1.
+ InternAtomCompletion InternOrGetAtomByName(absl::string_view name) noexcept {
+ return InternAtom(name, /*only_if_exists=*/false);
+ }
+
+ // Per the X specification, name should be encoded as ISO 8859-1.
+ InternAtomCompletion GetAtomByName(absl::string_view name) noexcept {
+ return InternAtom(name, /*only_if_exists=*/true);
+ }
+
+ VoidCompletion SendEvent(const Event&, bool propagate, Id destination_window,
+ const std::vector<EventMask>&);
+
+ // Escape hatches: raw handles to the X server as Xlib and XCB objects. Use
+ // these to interface with other libraries.
+ //
+ // Be careful with the Xlib handle--XCB, not Xlib, owns the event loop.
+ Display* AsXlibDisplay() noexcept { return xlib_; }
+ xcb_connection_t* AsXcbConnection() noexcept { return xcb_; }
+
+ private:
+ InternAtomCompletion InternAtom(absl::string_view name,
+ bool only_if_exists) noexcept {
+ return InternAtomCompletion(
+ xcb_, xcb_intern_atom(xcb_, only_if_exists, name.size(), name.data()));
+ }
+
+ Display* xlib_;
+ xcb_connection_t* xcb_;
+};
+
+// A class that monitors for X events. You probably only want one of these for
+// each connection; otherwise, events will be delivered nondeterministically to
+// the monitors.
+//
+// This class is thread-safe.
+class EventMonitor final {
+ public:
+ // Starts monitoring for X events on the specified connection.
+ explicit EventMonitor(Connection&);
+
+ EventMonitor(EventMonitor&&) noexcept = default;
+ EventMonitor& operator=(EventMonitor&&) noexcept = default;
+
+ ~EventMonitor();
+
+ absl::optional<Event> GetEventIfReady() noexcept ABSL_LOCKS_EXCLUDED(mu_) {
+ return WaitForEventWithTimeout(absl::ZeroDuration());
+ }
+
+ Event WaitForEvent() noexcept ABSL_LOCKS_EXCLUDED(mu_) {
+ return *WaitForEventWithTimeout(absl::InfiniteDuration());
+ }
+
+ absl::optional<Event> WaitForEventWithTimeout(absl::Duration) noexcept
+ ABSL_LOCKS_EXCLUDED(mu_);
+
+ private:
+ static constexpr absl::string_view kDoneAtomName =
+ "_GLPLANET_SRC_X_CONNECTION_EVENTMONITOR_DONE";
+
+ void WatcherThreadMain() noexcept ABSL_LOCKS_EXCLUDED(mu_);
+
+ bool EventsPresent() const noexcept {
+ mu_.AssertReaderHeld();
+ return !pending_events_.empty();
+ };
+
+ Connection& x_;
+ Id communication_window_;
+ Id close_connection_atom_;
+
+ absl::Mutex mu_;
+ std::deque<Event> pending_events_ ABSL_GUARDED_BY(mu_);
+
+ absl::optional<std::thread> watcher_thread_;
+};
+
+} // namespace x
+
+#endif // GLPLANET_SRC_X_CONNECTION_H_
diff --git a/src/x/error.cc b/src/x/error.cc
new file mode 100644
index 0000000..5b060c8
--- /dev/null
+++ b/src/x/error.cc
@@ -0,0 +1,76 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/x/error.h"
+
+#include <stdint.h>
+#include <xcb/xcb.h>
+
+#include <stdexcept>
+
+#include "src/util.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+
+namespace x {
+
+namespace {
+
+const char* StatusCodeName(uint8_t code) noexcept {
+ switch (code) {
+ case XCB_REQUEST:
+ return "Request";
+ case XCB_VALUE:
+ return "Value";
+ case XCB_WINDOW:
+ return "Window";
+ case XCB_PIXMAP:
+ return "Pixmap";
+ case XCB_ATOM:
+ return "Atom";
+ case XCB_CURSOR:
+ return "Cursor";
+ case XCB_FONT:
+ return "Font";
+ case XCB_MATCH:
+ return "Match";
+ case XCB_DRAWABLE:
+ return "Drawable";
+ case XCB_ACCESS:
+ return "Access";
+ case XCB_ALLOC:
+ return "Alloc";
+ case XCB_COLORMAP:
+ return "Colormap";
+ case XCB_G_CONTEXT:
+ return "XCB_Context";
+ case XCB_ID_CHOICE:
+ return "XCB_Choice";
+ case XCB_NAME:
+ return "Name";
+ case XCB_LENGTH:
+ return "Length";
+ case XCB_IMPLEMENTATION:
+ return "Implementation";
+ default:
+ DCHECK(false);
+ return "unknown error";
+ }
+}
+
+} // namespace
+
+Error::Error(uint8_t code) noexcept
+ : std::runtime_error(absl::StrCat("X error: ", StatusCodeName(code))) {}
+
+} // namespace x
diff --git a/src/x/error.h b/src/x/error.h
new file mode 100644
index 0000000..13510e8
--- /dev/null
+++ b/src/x/error.h
@@ -0,0 +1,32 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_X_ERROR_H_
+#define GLPLANET_SRC_X_ERROR_H_
+
+#include <stdint.h>
+
+#include <stdexcept>
+
+namespace x {
+
+// An X protocol error.
+class Error final : public std::runtime_error {
+ public:
+ explicit Error(uint8_t code) noexcept;
+};
+
+} // namespace x
+
+#endif // GLPLANET_SRC_X_ERROR_H_
diff --git a/src/x/event.cc b/src/x/event.cc
new file mode 100644
index 0000000..8346b40
--- /dev/null
+++ b/src/x/event.cc
@@ -0,0 +1,59 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/x/event.h"
+
+#include <stdint.h>
+#include <string.h>
+#include <xcb/xcb.h>
+
+#include <algorithm>
+#include <array>
+#include <stdexcept>
+
+#include "third_party/abseil/absl/types/variant.h"
+
+namespace x {
+
+namespace {
+
+constexpr uint8_t kXcbResponseTypeMask = 0x7f;
+
+} // namespace
+
+Event FromXcbGenericEvent(const xcb_generic_event_t& event) noexcept {
+ switch (event.response_type & kXcbResponseTypeMask) {
+ case XCB_EXPOSE:
+ return ExposeEvent(event);
+ case XCB_CONFIGURE_NOTIFY:
+ return ConfigureNotifyEvent(event);
+ case XCB_CLIENT_MESSAGE:
+ return ClientMessageEvent(event);
+ default:
+ return UnknownEvent(event);
+ }
+}
+
+std::array<char, 32> SerializeEvent(const Event& event) noexcept {
+ std::array<char, 32> raw = {};
+ absl::visit(
+ [&](auto&& ev) {
+ const auto& xcb_ev = ev.AsXcbEvent();
+ memcpy(raw.data(), &xcb_ev, std::min(raw.size(), sizeof(xcb_ev)));
+ },
+ event);
+ return raw;
+}
+
+} // namespace x
diff --git a/src/x/event.h b/src/x/event.h
new file mode 100644
index 0000000..8b9dd24
--- /dev/null
+++ b/src/x/event.h
@@ -0,0 +1,137 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// X events.
+
+#ifndef GLPLANET_SRC_X_EVENT_H_
+#define GLPLANET_SRC_X_EVENT_H_
+
+#include <string.h>
+#include <xcb/xcb.h>
+
+#include <array>
+
+#include "src/x/types.h"
+#include "third_party/abseil/absl/types/variant.h"
+
+namespace x_internal {
+
+template <typename From, typename To>
+void XcbDowncast(const From& from, To& to) noexcept {
+ static_assert(sizeof(From) >= sizeof(To));
+ memcpy(&to, &from, sizeof(To));
+}
+
+} // namespace x_internal
+
+namespace x {
+
+class ExposeEvent final {
+ public:
+ explicit ExposeEvent(const xcb_expose_event_t& event) noexcept
+ : event_(event) {}
+ explicit ExposeEvent(const xcb_generic_event_t& event) noexcept {
+ x_internal::XcbDowncast(event, event_);
+ }
+
+ ExposeEvent(const ExposeEvent&) noexcept = default;
+ ExposeEvent& operator=(const ExposeEvent&) noexcept = default;
+ ExposeEvent(ExposeEvent&&) noexcept = default;
+ ExposeEvent& operator=(ExposeEvent&&) noexcept = default;
+
+ xcb_expose_event_t& AsXcbEvent() noexcept { return event_; }
+ const xcb_expose_event_t& AsXcbEvent() const noexcept { return event_; }
+
+ private:
+ xcb_expose_event_t event_;
+};
+
+class ConfigureNotifyEvent final {
+ public:
+ explicit ConfigureNotifyEvent(
+ const xcb_configure_notify_event_t& event) noexcept
+ : event_(event) {}
+ explicit ConfigureNotifyEvent(const xcb_generic_event_t& event) noexcept {
+ x_internal::XcbDowncast(event, event_);
+ }
+
+ ConfigureNotifyEvent(const ConfigureNotifyEvent&) noexcept = default;
+ ConfigureNotifyEvent& operator=(const ConfigureNotifyEvent&) noexcept =
+ default;
+ ConfigureNotifyEvent(ConfigureNotifyEvent&&) noexcept = default;
+ ConfigureNotifyEvent& operator=(ConfigureNotifyEvent&&) noexcept = default;
+
+ int width() const noexcept { return event_.width; }
+ int height() const noexcept { return event_.height; }
+
+ xcb_configure_notify_event_t& AsXcbEvent() noexcept { return event_; }
+ const xcb_configure_notify_event_t& AsXcbEvent() const noexcept {
+ return event_;
+ }
+
+ private:
+ xcb_configure_notify_event_t event_;
+};
+
+class ClientMessageEvent final {
+ public:
+ explicit ClientMessageEvent(const xcb_client_message_event_t& event) noexcept
+ : event_(event) {}
+ explicit ClientMessageEvent(const xcb_generic_event_t& event) noexcept {
+ x_internal::XcbDowncast(event, event_);
+ }
+
+ ClientMessageEvent(const ClientMessageEvent&) noexcept = default;
+ ClientMessageEvent& operator=(const ClientMessageEvent&) noexcept = default;
+ ClientMessageEvent(ClientMessageEvent&&) noexcept = default;
+ ClientMessageEvent& operator=(ClientMessageEvent&&) noexcept = default;
+
+ Id type() const noexcept { return event_.type; }
+
+ xcb_client_message_event_t& AsXcbEvent() noexcept { return event_; }
+ const xcb_client_message_event_t& AsXcbEvent() const noexcept {
+ return event_;
+ }
+
+ private:
+ xcb_client_message_event_t event_;
+};
+
+// An X event that doesn't fit into any of the previous classes.
+class UnknownEvent final {
+ public:
+ explicit UnknownEvent(const xcb_generic_event_t& event) : event_(event) {}
+
+ UnknownEvent(const UnknownEvent&) noexcept = default;
+ UnknownEvent& operator=(const UnknownEvent&) noexcept = default;
+ UnknownEvent(UnknownEvent&&) noexcept = default;
+ UnknownEvent& operator=(UnknownEvent&&) noexcept = default;
+
+ xcb_generic_event_t& AsXcbEvent() noexcept { return event_; }
+ const xcb_generic_event_t& AsXcbEvent() const noexcept { return event_; }
+
+ private:
+ xcb_generic_event_t event_;
+};
+
+using Event = absl::variant<UnknownEvent, ExposeEvent, ConfigureNotifyEvent,
+ ClientMessageEvent>;
+
+Event FromXcbGenericEvent(const xcb_generic_event_t&) noexcept;
+
+std::array<char, 32> SerializeEvent(const Event&) noexcept;
+
+} // namespace x
+
+#endif // GLPLANET_SRC_X_EVENT_H_
diff --git a/src/x/init.cc b/src/x/init.cc
new file mode 100644
index 0000000..577f074
--- /dev/null
+++ b/src/x/init.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/x/init.h"
+
+#include <X11/Xlib.h>
+
+#include <stdexcept>
+
+#include "src/undo_xlib_dot_h_namespace_pollution.h"
+//
+
+namespace x {
+
+void Initialize() {
+ if (!XInitThreads()) {
+ throw std::runtime_error(
+ "X: could not initialize Xlib support for concurrent threads");
+ }
+}
+
+} // namespace x
diff --git a/src/x/init.h b/src/x/init.h
new file mode 100644
index 0000000..332d450
--- /dev/null
+++ b/src/x/init.h
@@ -0,0 +1,26 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_X_INIT_H_
+#define GLPLANET_SRC_X_INIT_H_
+
+namespace x {
+
+// You must call this function before using any of the code in this library.
+// (Call it at the start of main.)
+void Initialize();
+
+} // namespace x
+
+#endif // GLPLANET_SRC_X_INIT_H_
diff --git a/src/x/rpc.cc b/src/x/rpc.cc
new file mode 100644
index 0000000..7786d9b
--- /dev/null
+++ b/src/x/rpc.cc
@@ -0,0 +1,53 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "src/x/rpc.h"
+
+#include <xcb/xcb.h>
+
+#include <memory>
+
+#include "src/x/error.h"
+#include "src/x/types.h"
+#include "src/x/util.h"
+
+namespace x {
+
+namespace {
+
+template <typename T>
+using ManagedPtr = std::unique_ptr<T, x_internal::FreeDeleter>;
+
+} // namespace
+
+void VoidCompletion::Check() const&& {
+ auto error =
+ ManagedPtr<xcb_generic_error_t>(xcb_request_check(xcb_, cookie_));
+ if (error != nullptr) {
+ throw Error(error->error_code);
+ }
+}
+
+Id InternAtomCompletion::Get() const&& {
+ xcb_generic_error_t* error_raw;
+ auto reply = ManagedPtr<xcb_intern_atom_reply_t>(
+ xcb_intern_atom_reply(xcb_, cookie_, &error_raw));
+ if (reply == nullptr) {
+ auto error = ManagedPtr<xcb_generic_error_t>(error_raw);
+ throw Error(error->error_code);
+ }
+ return reply->atom;
+}
+
+} // namespace x
diff --git a/src/x/rpc.h b/src/x/rpc.h
new file mode 100644
index 0000000..c2fa652
--- /dev/null
+++ b/src/x/rpc.h
@@ -0,0 +1,78 @@
+// Copyright 2021, 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// In-flight RPCs.
+//
+// Each of these classes represents an individual X RPC, possibly still in
+// flight. Inspecting an instance will block until the RPC has completed.
+
+#ifndef GLPLANET_SRC_X_RPC_H_
+#define GLPLANET_SRC_X_RPC_H_
+
+#include <xcb/xcb.h>
+
+#include "src/x/types.h"
+
+namespace x {
+
+// An RPC that returns nothing.
+//
+// This class is thread-safe.
+class VoidCompletion final {
+ public:
+ explicit VoidCompletion(xcb_connection_t* xcb,
+ xcb_void_cookie_t cookie) noexcept
+ : xcb_(xcb), cookie_(cookie) {}
+
+ VoidCompletion(const VoidCompletion&) noexcept = default;
+ VoidCompletion& operator=(const VoidCompletion&) noexcept = default;
+ VoidCompletion(VoidCompletion&&) noexcept = default;
+ VoidCompletion& operator=(VoidCompletion&&) noexcept = default;
+
+ // Blocks until the RPC completes and checks to make sure it was successful.
+ // Throws Error if it was not.
+ void Check() const&&;
+
+ private:
+ xcb_connection_t* xcb_;
+ xcb_void_cookie_t cookie_;
+};
+
+// An InternAtom RPC.
+//
+// This class is thread-safe.
+class InternAtomCompletion final {
+ public:
+ explicit InternAtomCompletion(xcb_connection_t* xcb,
+ xcb_intern_atom_cookie_t cookie) noexcept
+ : xcb_(xcb), cookie_(cookie) {}
+
+ InternAtomCompletion(const InternAtomCompletion&) noexcept = default;
+ InternAtomCompletion& operator=(const InternAtomCompletion&) noexcept =
+ default;
+ InternAtomCompletion(InternAtomCompletion&&) noexcept = default;
+ InternAtomCompletion& operator=(InternAtomCompletion&&) noexcept = default;
+
+ // Blocks until the RPC completes, checks to make sure it was successful, and
+ // returns the atom. Throws Error if the RPC failed.
+ Id Get() const&&;
+
+ private:
+ xcb_connection_t* xcb_;
+ xcb_intern_atom_cookie_t cookie_;
+};
+
+} // namespace x
+
+#endif // GLPLANET_SRC_X_RPC_H_
diff --git a/src/x/screen.h b/src/x/screen.h
new file mode 100644
index 0000000..88acae5
--- /dev/null
+++ b/src/x/screen.h
@@ -0,0 +1,51 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_X_SCREEN_H_
+#define GLPLANET_SRC_X_SCREEN_H_
+
+#include <stdint.h>
+#include <xcb/xcb.h>
+#include <xcb/xcb_aux.h>
+
+#include "src/x/types.h"
+
+namespace x {
+
+// An X screen.
+//
+// This class is thread-safe.
+class Screen final {
+ public:
+ explicit Screen(xcb_screen_t* screen) noexcept : screen_(screen) {}
+
+ Screen(Screen&&) noexcept = default;
+ Screen& operator=(Screen&&) noexcept = default;
+
+ Id root() const noexcept { return screen_->root; }
+ Id black_pixel() const noexcept { return screen_->black_pixel; }
+
+ uint8_t DepthOfVisual(Id visual) const noexcept {
+ return xcb_aux_get_depth_of_visual(screen_, visual);
+ }
+
+ xcb_screen_t* AsXcbScreen() const noexcept { return screen_; }
+
+ private:
+ xcb_screen_t* screen_;
+};
+
+} // namespace x
+
+#endif // GLPLANET_SRC_X_SCREEN_H_
diff --git a/src/x/types.h b/src/x/types.h
new file mode 100644
index 0000000..441df89
--- /dev/null
+++ b/src/x/types.h
@@ -0,0 +1,28 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_X_TYPES_H_
+#define GLPLANET_SRC_X_TYPES_H_
+
+#include <stdint.h>
+
+namespace x {
+
+// An X protocol identifier. These are used for all kinds of X
+// resources--windows, colormaps, etc.
+using Id = uint32_t;
+
+} // namespace x
+
+#endif // GLPLANET_SRC_X_TYPES_H_
diff --git a/src/x/util.h b/src/x/util.h
new file mode 100644
index 0000000..5dbb1a0
--- /dev/null
+++ b/src/x/util.h
@@ -0,0 +1,33 @@
+// Copyright 2021 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#ifndef GLPLANET_SRC_X_UTIL_H_
+#define GLPLANET_SRC_X_UTIL_H_
+
+#include <stdlib.h>
+
+namespace x_internal {
+
+// A deleter for std::unique_ptr that `free`s instead of calling `delete`.
+class FreeDeleter final {
+ public:
+ template <typename T>
+ void operator()(T* p) noexcept {
+ free(p);
+ }
+};
+
+} // namespace x_internal
+
+#endif // GLPLANET_SRC_X_UTIL_H_
diff --git a/tex/clouds.webp b/tex/clouds.webp
new file mode 100644
index 0000000..e316727
--- /dev/null
+++ b/tex/clouds.webp
Binary files differ
diff --git a/tex/earth.webp b/tex/earth.webp
new file mode 100644
index 0000000..7e9ad1d
--- /dev/null
+++ b/tex/earth.webp
Binary files differ
diff --git a/third_party/abseil b/third_party/abseil
new file mode 160000
+Subproject 385b5ccafdabf0ca91797ae3caa755ab7451c45
diff --git a/third_party/date b/third_party/date
new file mode 160000
+Subproject 3776e0f18562b8813a8733aed3677e01e960a5a
diff --git a/third_party/glew b/third_party/glew
new file mode 160000
+Subproject 4fbb8fe14aa95cf0051b70e42913118adbf3a29