diff options
author | Benjamin Barenblat <bbarenblat@gmail.com> | 2022-01-17 23:12:32 -0500 |
---|---|---|
committer | Benjamin Barenblat <bbarenblat@gmail.com> | 2022-01-30 15:55:27 -0500 |
commit | d0e18bdb7924c71cdca8dd983711171d87ef28be (patch) | |
tree | 6be11aae0b7c8874e6507b75b4ef26d1353952c7 /src |
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.
Diffstat (limited to 'src')
48 files changed, 5282 insertions, 0 deletions
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_ |