Compare commits

...

10 commits

Author SHA1 Message Date
0bc2decb7c
docs: add project README
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a696411e599829afb123a5f3c241768470163
2025-09-30 20:11:50 +03:00
edc7552b5c
add sample configuration
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a696450749c525482d2eab12de1a0e520e973
2025-09-30 20:11:49 +03:00
e72da82b32
meta: set vendored code manually
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964fbd3dbcc49825f381f980d3fd76e8766
2025-09-30 20:11:48 +03:00
d1116e7721
render: add OpenGL resource caching; optimize texture handling
Mildly improves rendering performance by caching OpenGL resources.
Namely:

- Cache shader program, VBO/EBO, and textures per output
- Automatically free image data after GPU upload
- Force RGBA format for consistent texture handling
- Track texture state across output changes
- Add texture invalidation on image changes

This reduces the memory usage by a solid 30MB, but it's still not quite
enough. I expect (or rather, hope) that we can cut it by half.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964eebc783c5bc07b1fef7548a8d49f529c
2025-09-30 20:11:47 +03:00
3cbf6d5645
chore: add systemd service
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964d88b3de5f79306867e38dfbfef2ee1af
2025-09-30 20:11:46 +03:00
b47928cbab
meta: release under MPL v2.0
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964c2608feafccbc9ec9214a7bdb845dbb5
2025-09-30 20:11:45 +03:00
87d445340a
chore: set up Makefile for building
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964b70cec53a4e831118fabed1ecbd5c317
2025-09-30 20:11:44 +03:00
b4fcdab7f4
meta: correctly ignore corrected files
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964a2b8fd7724999f28432a8b12c6143e54
2025-09-30 20:11:43 +03:00
adc2bb2baf
meta: set up editorconfig
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964b4f245552027208a2650c8d60fa557a4
2025-09-30 20:11:42 +03:00
48fa1cc852
init nix tooling
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964a17ac94c2a40f0fd34f339a965abfc9a
2025-09-30 20:11:41 +03:00
16 changed files with 1154 additions and 114 deletions

33
.editorconfig Normal file
View file

@ -0,0 +1,33 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.c]
indent_style = space
indent_size = 2
tab_width = 2
[*.h]
indent_style = space
indent_size = 2
tab_width = 2
[Makefile]
indent_style = tab
tab_width = 4
trim_trailing_whitespace = false
[*.mk]
indent_style = tab
tab_width = 4
trim_trailing_whitespace = false
[*.sh]
indent_style = space
indent_size = 2
tab_width = 2

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

8
.gitattributes vendored Normal file
View file

@ -0,0 +1,8 @@
# Vendored headers are vendored code, to the surprise of absolutely noone.
# See:
# <https://github.com/github-linguist/linguist/blob/main/docs/overrides.md#vendored-code>
/include/chroma.h linguist-vendored
# Don't think linguist can detect XML, but let's tell it that the protocols are
# vendored *anyway*.
/protocols/*.xml linguist-vendored

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
/bin
/obj
# Generated by wayland-scanner
include/xdg-shell.h
include/wlr-layer-shell-unstable-v1.h
src/xdg-shell.c
src/wlr-layer-shell-unstable-v1.c

328
LICENSE Normal file
View file

@ -0,0 +1,328 @@
Mozilla Public License, version 2.0
1. Definitions
1.1. “Contributor”
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. “Contributor Version”
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributors Contribution.
1.3. “Contribution”
means Covered Software of a particular Contributor.
1.4. “Covered Software”
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form,
and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. “Incompatible With Secondary Licenses”
means
a. that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the terms
of a Secondary License.
1.6. “Executable Form”
means any form of the work other than Source Code Form.
1.7. “Larger Work”
means a work that combines Covered Software with other material,
in a separate file or files, that is not Covered Software.
1.8. “License”
means this document.
1.9. “Licensable”
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently,
any and all of the rights conveyed by this License.
1.10. “Modifications”
means any of the following:
a. any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. “Patent Claims” of a Contributor
means any patent claim(s), including without limitation, method, process,
and apparatus claims, in any patent Licensable by such Contributor that
would be infringed, but for the grant of the License, by the making,
using, selling, offering for sale, having made, import, or transfer of
either its Contributions or its Contributor Version.
1.12. “Secondary License”
means either the GNU General Public License, Version 2.0, the
GNU Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those licenses.
1.13. “Source Code Form”
means the form of the work preferred for making modifications.
1.14. “You” (or “Your”)
means an individual or a legal entity exercising rights under this License.
For legal entities, “You” includes any entity that controls,
is controlled by, or is under common control with You. For purposes of
this definition, “control” means (a) the power, direct or indirect,
to cause the direction or management of such entity, whether by contract
or otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications,
or as part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell,
offer for sale, have made, import, and otherwise transfer either
its Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor
first distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted
under this License. No additional rights or licenses will be implied
from the distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted
by a Contributor:
a. for any code that a Contributor has removed from
Covered Software; or
b. for infringements caused by: (i) Your and any other third partys
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its
Contributor Version); or
c. under Patent Claims infringed by Covered Software in the
absence of its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License
(if permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing,
or other equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the
licenses granted in Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including
any Modifications that You create or to which You contribute, must be
under the terms of this License. You must inform recipients that the
Source Code Form of the Covered Software is governed by the terms
of this License, and how they can obtain a copy of this License.
You may not attempt to alter or restrict the recipients rights
in the Source Code Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more than
the cost of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of
Covered Software with a work governed by one or more Secondary Licenses,
and the Covered Software is not Incompatible With Secondary Licenses,
this License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the
Covered Software under the terms of either this License or such
Secondary License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of
Covered Software. However, You may do so only on Your own behalf,
and not on behalf of any Contributor. You must make it absolutely clear
that any such warranty, support, indemnity, or liability obligation is
offered by You alone, and You hereby agree to indemnify every Contributor
for any liability incurred by such Contributor as a result of warranty,
support, indemnity or liability terms You offer. You may include
additional disclaimers of warranty and limitations of liability
specific to any jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute,
judicial order, or regulation then You must: (a) comply with the terms of
this License to the maximum extent possible; and (b) describe the limitations
and the code they affect. Such description must be placed in a text file
included with all distributions of the Covered Software under this License.
Except to the extent prohibited by statute or regulation, such description
must be sufficiently detailed for a recipient of ordinary skill
to be able to understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means,
this is the first time You have received notice of non-compliance with
this License from such Contributor, and You become compliant prior to
30 days after Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted
to You by any and all Contributors for the Covered Software under
Section 2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an “as is” basis, without
warranty of any kind, either expressed, implied, or statutory, including,
without limitation, warranties that the Covered Software is free of defects,
merchantable, fit for a particular purpose or non-infringing. The entire risk
as to the quality and performance of the Covered Software is with You.
Should any Covered Software prove defective in any respect, You
(not any Contributor) assume the cost of any necessary servicing, repair,
or correction. This disclaimer of warranty constitutes an essential part of
this License. No use of any Covered Software is authorized under this
License except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort
(including negligence), contract, or otherwise, shall any Contributor, or
anyone who distributes Covered Software as permitted above, be liable to
You for any direct, indirect, special, incidental, or consequential damages
of any character including, without limitation, damages for lost profits,
loss of goodwill, work stoppage, computer failure or malfunction, or any and
all other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from
such partys negligence to the extent applicable law prohibits such
limitation. Some jurisdictions do not allow the exclusion or limitation of
incidental or consequential damages, so this exclusion and limitation may
not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts of
a jurisdiction where the defendant maintains its principal place of business
and such litigation shall be governed by laws of that jurisdiction, without
reference to its conflict-of-law provisions. Nothing in this Section shall
prevent a partys ability to bring cross-claims or counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject matter
hereof. If any provision of this License is held to be unenforceable,
such provision shall be reformed only to the extent necessary to make it
enforceable. Any law or regulation which provides that the language of a
contract shall be construed against the drafter shall not be used to construe
this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in
Section 10.3, no one other than the license steward has the right to
modify or publish new versions of this License. Each version will be
given a distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published
by the license steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a modified
version of this License if you rename the license and remove any
references to the name of the license steward (except to note that such
modified license differs from this License).
10.4. Distributing Source Code Form that is
Incompatible With Secondary Licenses
If You choose to distribute Source Code Form that is
Incompatible With Secondary Licenses under the terms of this version of
the License, the notice described in Exhibit B of this
License must be attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to
look for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - “Incompatible With Secondary Licenses” Notice
This Source Code Form is “Incompatible With Secondary Licenses”,
as defined by the Mozilla Public License, v. 2.0.

173
Makefile Normal file
View file

@ -0,0 +1,173 @@
PROJECT_NAME = chroma
VERSION = 1.0.0
# Directories
SRCDIR = src
INCDIR = include
OBJDIR = obj
BINDIR = bin
SYSTEMD_DIR = systemd
PROTOCOLDIR = protocols
# Install directories
PREFIX ?= /usr/local
BINDIR_INSTALL = $(PREFIX)/bin
SYSTEMD_INSTALL = $(HOME)/.config/systemd/user
# Compiler and flags
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g
CFLAGS += -D_GNU_SOURCE -DCHROMA_VERSION=\"$(VERSION)\"
CPPFLAGS = -I$(INCDIR)
# Debug build flags
DEBUG_CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -Og -g3 -DDEBUG
DEBUG_CFLAGS += -D_GNU_SOURCE -DCHROMA_VERSION=\"$(VERSION)-debug\"
DEBUG_CFLAGS += -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer
# Libraries using pkg-config
PKG_DEPS = wayland-client wayland-egl egl glesv2 wayland-protocols
CFLAGS += $(shell pkg-config --cflags $(PKG_DEPS))
LDFLAGS += $(shell pkg-config --libs $(PKG_DEPS))
# Protocol files
PROTOCOL_XML = $(PROTOCOLDIR)/xdg-shell.xml $(PROTOCOLDIR)/wlr-layer-shell-unstable-v1.xml
PROTOCOL_HEADERS = $(PROTOCOL_XML:$(PROTOCOLDIR)/%.xml=$(INCDIR)/%.h)
PROTOCOL_SOURCES = $(PROTOCOL_XML:$(PROTOCOLDIR)/%.xml=$(SRCDIR)/%.c)
PROTOCOL_OBJECTS = $(PROTOCOL_SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
# Additional libraries
LDFLAGS += -lm -ldl
# Source files (excluding generated protocol files)
SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c))
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS)
DEPENDS = $(OBJECTS:.o=.d)
# Default target
TARGET = $(BINDIR)/$(PROJECT_NAME)
all: $(TARGET)
# Create directories
$(OBJDIR):
@mkdir -p $(OBJDIR)
$(BINDIR):
@mkdir -p $(BINDIR)
# Protocol generation
$(INCDIR)/%.h: $(PROTOCOLDIR)/%.xml | $(INCDIR)
@echo " PROTO $<"
@wayland-scanner client-header $< $@
$(SRCDIR)/%.c: $(PROTOCOLDIR)/%.xml
@echo " PROTO $<"
@wayland-scanner private-code $< $@
# Make sure include directory exists
$(INCDIR):
@mkdir -p $(INCDIR)
# Build main executable
$(TARGET): $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR)
@echo " LINK $@"
@$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
# Compile source files
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR)
@echo " CC $<"
@$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
# Debug build
debug: CFLAGS = $(DEBUG_CFLAGS)
debug: $(TARGET)
# Static build (for distribution)
static: LDFLAGS += -static
static: $(TARGET)
# Check if required dependencies are available
check-deps:
@echo "Checking dependencies..."
@pkg-config --exists $(PKG_DEPS) || (echo "Missing dependencies. Install: wayland-protocols libwayland-dev libegl1-mesa-dev libgl1-mesa-dev wayland-scanner" && exit 1)
@which wayland-scanner >/dev/null || (echo "wayland-scanner not found. Please install wayland-scanner." && exit 1)
@echo "All dependencies found."
# Install
install: $(TARGET)
@echo "Installing $(PROJECT_NAME)..."
install -Dm755 $(TARGET) $(DESTDIR)$(BINDIR_INSTALL)/$(PROJECT_NAME)
@if [ -f $(SYSTEMD_DIR)/$(PROJECT_NAME).service ]; then \
echo "Installing systemd service..."; \
install -Dm644 $(SYSTEMD_DIR)/$(PROJECT_NAME).service $(DESTDIR)$(SYSTEMD_INSTALL)/$(PROJECT_NAME).service; \
fi
@echo "Installation complete."
# Uninstall
uninstall:
@echo "Uninstalling $(PROJECT_NAME)..."
rm -f $(DESTDIR)$(BINDIR_INSTALL)/$(PROJECT_NAME)
rm -f $(DESTDIR)$(SYSTEMD_INSTALL)/$(PROJECT_NAME).service
@echo "Uninstall complete."
# Create systemd service file
systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
rm -rf $(OBJDIR) $(BINDIR)
rm -f $(PROTOCOL_HEADERS) $(PROTOCOL_SOURCES)
# Format source code (requires clang-format)
format:
@echo "Formatting source code..."
@find $(SRCDIR) $(INCDIR) -name "*.c" -o -name "*.h" | xargs clang-format -i
# Static analysis (requires cppcheck)
analyze:
@echo "Running static analysis..."
@cppcheck --enable=all --std=c11 --language=c \
--include=$(INCDIR) \
--suppress=missingIncludeSystem \
--quiet \
$(SRCDIR)
# Run tests
# FIXME: add tests
test: $(TARGET)
@echo "Running tests..."
@echo "Tests not implemented yet."
# Help target
help:
@echo "Available targets:"
@echo " all - Build the main executable (default)"
@echo " debug - Build with debug symbols and sanitizers"
@echo " static - Build statically linked executable"
@echo " check-deps - Check if all dependencies are available"
@echo " install - Install the executable and systemd service"
@echo " uninstall - Remove installed files"
@echo " clean - Remove build artifacts"
@echo " distclean - Remove all generated files"
@echo " format - Format source code (requires clang-format)"
@echo " analyze - Run static analysis (requires cppcheck)"
@echo " test - Run tests"
@echo " help - Show this help message"
@echo ""
@echo "Examples:"
@echo " make # Build with default settings"
@echo " make debug # Build debug version"
@echo " make PREFIX=/usr install # Install to /usr instead of /usr/local"
@echo " make CC=clang # Use clang instead of gcc"
# Include dependency files
-include $(DEPENDS)
# Phony targets
.PHONY: all debug static check-deps install uninstall systemd-service sample-config clean distclean format analyze test help
# Print variables
print-%:
@echo '$*=$($*)'

211
README.md Normal file
View file

@ -0,0 +1,211 @@
# Chroma
A simple, lightweight and efficientt wallpaper daeemon for Wayland compositors
with multi-monitor & automatic hotplugging support. Born from my woes with
Hyprpaper, swaybg and most ironically SWWW, which turned out to be NOT a
solution to my Wayland wallpaper woes.
## Features
I did not expect to be writing something too feature-rich, but I still ended up
with something relatively convoluted. Chroma (mostly) reliably supports:
- **Multi-monitors**: Automatically detects and manages wallpapers for all
connected displays
- **Hotplugging**: Dynamically handles monitor connection/disconnection events
- **Per-output configuration**: Set different wallpapers for each monitor
- **Multiple image formats**: Supports JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC,
PPM, PGM
- **EGL/OpenGL rendering**: Hardware-accelerated wallpaper rendering
- **Configuration file support**: Easy setup with INI-style config files
- **Signal handling**: Graceful shutdown and configuration reload (SIGHUP)
## Usage
### Requirements
#### Dependencies
- **Wayland**: wayland-client, wayland-egl
- **Graphics**: EGL, OpenGL
- **System**: glibc, libm, libdl
#### Development Dependencies
- GCC or Clang compiler
- Make
- pkg-config
- Wayland development headers
- EGL/OpenGL development headers
### Building
#### Quick Build
```bash
# Check dependencies
make check-deps
# Build the daemon
make
# Or build debug version
make debug
```
### Installation
```bash
# Install to /usr/local (default)
sudo make install
# Install to /usr
sudo make PREFIX=/usr install
# Create systemd service file
make systemd-service
# Create sample configuration
make sample-config
```
### Configuration
#### Configuration File
Chroma looks for configuration files in this order:
1. `~/.config/chroma/chroma.conf`
2. `$XDG_CONFIG_HOME/chroma/chroma.conf`
3. `./chroma.conf` (current directory)
#### Sample Configuration
```ini
# Default wallpaper for outputs without specific mapping
default_image = ~/.config/chroma/default.jpg
# Output-specific wallpapers
# Format: output.OUTPUT_NAME = /path/to/image.jpg
output.DP-1 = ~/Pictures/monitor1.jpg
output.DP-2 = ~/Pictures/monitor2.png
output.HDMI-A-1 = ~/Pictures/hdmi_wallpaper.jpg
```
### Finding Output Names
Use `wlr-randr` or similar tools to find your output names:
```bash
wlr-randr
```
### Command Line Options
```plaintext
Usage: chroma [OPTIONS]
Options:
-c, --config FILE Configuration file path
-d, --daemon Run as daemon
-v, --verbose Verbose logging
-h, --help Show help
--version Show version information
Examples:
chroma -c ~/.config/chroma/chroma.conf
chroma --daemon
```
### Running Manually
```bash
# Run in foreground
chroma
# Run as daemon
chroma --daemon
# Use custom config
chroma -c /path/to/config.conf
```
### Systemd Service
```bash
# Enable and start the service
systemctl --user enable chroma.service
systemctl --user start chroma.service
# Check status
systemctl --user status chroma.service
# View logs
journalctl --user -u chroma.service -f
# Reload configuration
systemctl --user reload chroma.service
```
### Auto-start with Desktop Session
```bash
# Enable the service for auto-start
systemctl --user enable chroma.service
# The service will start automatically with your graphical session
```
### Supported Wayland Compositors
Chroma works with any Wayland compositor that supports:
- `wl_compositor` interface
- `wl_output` interface
- EGL window surface creation
Tested only on Hyprland.
## Development
### Building Debug Version
```bash
make debug
```
### Code Formatting
```bash
make format # requires clang-format
```
### Static Analysis
```bash
make analyze # requires cppcheck
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
### Code Style
- C11 standard
- 2-space indentation
- No tabs
- Function names: `chroma_function_name`
- Constants: `CHROMA_CONSTANT_NAME`
## License
<!--markdownlint-disable MD059 -->
This project is made available under Mozilla Public License (MPL) version 2.0.
See [LICENSE](LICENSE) for more details on the exact conditions. An online copy
is provided [here](https://www.mozilla.org/en-US/MPL/2.0/).

48
chroma.conf.sample Normal file
View file

@ -0,0 +1,48 @@
# This is a sample configuration file for the Chroma wallpaper daemon.
# Lines starting with # are comments and are ignored.
#
# Configuration file locations (checked in order):
# 1. ~/.config/chroma/chroma.conf
# 2. $XDG_CONFIG_HOME/chroma/chroma.conf
# 3. ./chroma.conf (current directory)
#
# To get started:
# 1. Copy this file to ~/.config/chroma/chroma.conf
# 2. Modify the paths to point to your wallpaper images
# 3. Use 'wlr-randr' or similar tools to find your output names
# 4. Restart chroma or send SIGHUP to reload configuration
# This image will be used for any output that doesn't have a specific mapping.
# Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM
# Paths can be absolute or relative, ~ expansion is supported.
#
# Alternative examples:
# default_image = ~/Pictures/wallpapers/default.png
# default_image = /usr/share/wallpapers/default.jpg
# default_image = ./wallpapers/fallback.jpg
default_image = ~/.config/chroma/default.jpg
# Whether to run as a background daemon
# Usually set via command line option --daemon, but can be set here too
daemon_mode = false
# Output-specific wallpaper mappings
# ==================================
# Format: output.OUTPUT_NAME = /path/to/image.ext
#
# To find your output names, run one of these commands:
# - wlr-randr (for wlroots-based compositors like Sway, Hyprland)
# - wayland-info | grep wl_output
# - kanshi list-outputs
#
# Common output name patterns:
# - DP-1, DP-2, DP-3, etc. (DisplayPort)
# - HDMI-A-1, HDMI-A-2, etc. (HDMI)
# - eDP-1 (embedded DisplayPort, laptops)
# - DVI-D-1, DVI-I-1 (DVI)
# - VGA-1 (VGA, legacy)
#
# Example:
# output.HDMI-A-1 = ~/Pictures/wallpaper.jpg

26
flake.lock generated Normal file
View file

@ -0,0 +1,26 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1759155593,
"narHash": "sha256-potIJyEY7ExgfiMzr44/KBODFednyzRUAE2vs4aThHs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "13ca7febc86978bdb67c0ae94f568b189ae84eef",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

22
flake.nix Normal file
View file

@ -0,0 +1,22 @@
{
description = "Wayland Wallpaper Daemon";
inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref?nixos-unstable";
outputs = {nixpkgs, ...}: let
systems = ["x86_64-linux" "aarch64-linux"];
forAllSystems = f:
builtins.listToAttrs (map (system: {
name = system;
value = f system;
})
systems);
pkgsFor = system: nixpkgs.legacyPackages.${system};
in {
devShells = forAllSystems (system: let
pkgs = pkgsFor system;
in {
default = pkgs.callPackage ./shell.nix {};
});
};
}

9
include/chroma.h vendored
View file

@ -62,6 +62,14 @@ typedef struct {
// Associated wallpaper // Associated wallpaper
chroma_image_t *image; chroma_image_t *image;
// OpenGL resource cache
GLuint texture_id;
GLuint shader_program;
GLuint vbo;
GLuint ebo;
bool gl_resources_initialized;
bool texture_uploaded;
} chroma_output_t; } chroma_output_t;
// Config mapping structure // Config mapping structure
@ -150,6 +158,7 @@ void chroma_egl_cleanup(chroma_state_t *state);
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output); int chroma_surface_create(chroma_state_t *state, chroma_output_t *output);
void chroma_surface_destroy(chroma_output_t *output); void chroma_surface_destroy(chroma_output_t *output);
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output); int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output);
void chroma_output_invalidate_texture(chroma_output_t *output);
// Layer shell functions // Layer shell functions
void chroma_layer_surface_configure(void *data, void chroma_layer_surface_configure(void *data,

51
shell.nix Normal file
View file

@ -0,0 +1,51 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
name = "chroma";
buildInputs = with pkgs; [
gnumake
# Optional development tools
gdb
valgrind
strace
# Code formatting and analysis
clang-tools # includes clang-format
cppcheck
# Wayland libraries
wayland.dev
wayland-protocols
wayland-scanner
libxkbcommon
# EGL/OpenGL libraries
libGL
mesa
# System libraries
glibc.dev
];
nativeBuildInputs = with pkgs; [
pkg-config
];
shellHook = ''
echo "Available commands:"
echo " make - Build the project"
echo " make debug - Build with debug symbols"
echo " make clean - Clean build artifacts"
echo " make install - Install to ~/.local"
echo " make help - Show all available targets"
echo
echo "Run 'make check-deps' to verify all dependencies are available."
echo
'';
# Environment variables for the build system
env = {
CHROMA_VERSION = "1.0.0";
WAYLAND_DEBUG = 1;
};
}

View file

@ -87,6 +87,14 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
return CHROMA_ERROR_IMAGE; return CHROMA_ERROR_IMAGE;
} }
// Check if image changed and invalidate texture cache if neceessary
bool image_changed = (output->image != image);
if (image_changed && output->image) {
chroma_output_invalidate_texture(output);
chroma_log("DEBUG", "Image changed for output %u, invalidated texture",
output->id);
}
// Assign image to output // Assign image to output
output->image = image; output->image = image;

View file

@ -49,11 +49,12 @@ int chroma_image_load(chroma_image_t *image, const char *path) {
(double)file_size / (1024.0 * 1024.0)); (double)file_size / (1024.0 * 1024.0));
} }
// Load image data using stb_image // Load image data using stb_image, force RGBA format to avoid conversion
stbi_set_flip_vertically_on_load(0); // Keep images right-side up stbi_set_flip_vertically_on_load(0); // keep images right-side up
image->data = image->data =
stbi_load(path, &image->width, &image->height, &image->channels, 0); stbi_load(path, &image->width, &image->height, &image->channels, 4);
image->channels = 4; // always RGBA after forced conversion
if (!image->data) { if (!image->data) {
chroma_log("ERROR", "Failed to load image %s: %s", path, chroma_log("ERROR", "Failed to load image %s: %s", path,
stbi_failure_reason()); stbi_failure_reason());
@ -68,39 +69,14 @@ int chroma_image_load(chroma_image_t *image, const char *path) {
return CHROMA_ERROR_IMAGE; return CHROMA_ERROR_IMAGE;
} }
// Check supported formats // Validate we have RGBA data (should always be true with forced conversion)
if (image->channels < 3 || image->channels > 4) { if (image->channels != 4) {
chroma_log("ERROR", chroma_log("ERROR", "Failed to load image as RGBA: got %d channels",
"Unsupported image format: %d channels (need RGB or RGBA)",
image->channels); image->channels);
chroma_image_free(image); chroma_image_free(image);
return CHROMA_ERROR_IMAGE; return CHROMA_ERROR_IMAGE;
} }
// Convert RGB to RGBA if necessary for consistent handling
if (image->channels == 3) {
int pixel_count = image->width * image->height;
unsigned char *rgba_data = malloc(pixel_count * 4);
if (!rgba_data) {
chroma_log("ERROR", "Failed to allocate memory for RGBA conversion");
chroma_image_free(image);
return CHROMA_ERROR_MEMORY;
}
// Convert RGB to RGBA
for (int i = 0; i < pixel_count; i++) {
rgba_data[i * 4 + 0] = image->data[i * 3 + 0]; // R
rgba_data[i * 4 + 1] = image->data[i * 3 + 1]; // G
rgba_data[i * 4 + 2] = image->data[i * 3 + 2]; // B
rgba_data[i * 4 + 3] = 255; // A
}
// Replace original data
stbi_image_free(image->data);
image->data = rgba_data;
image->channels = 4;
}
image->loaded = true; image->loaded = true;
chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)", path, chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)", path,
@ -118,13 +94,8 @@ void chroma_image_free(chroma_image_t *image) {
} }
if (image->data) { if (image->data) {
if (image->channels == 4 && strlen(image->path) > 0) { // Always use stbi_image_free since we load directly with stbi_load
// If we converted from RGB to RGBA, use regular free()
free(image->data);
} else {
// If loaded directly by stb_image, use stbi_image_free()
stbi_image_free(image->data); stbi_image_free(image->data);
}
image->data = NULL; image->data = NULL;
} }

View file

@ -1,4 +1,3 @@
#include <math.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -7,6 +6,7 @@
#include <GLES2/gl2.h> #include <GLES2/gl2.h>
#include "../include/chroma.h" #include "../include/chroma.h"
#include "../include/stb_image.h"
// Vertex shader for simple texture rendering // Vertex shader for simple texture rendering
static const char *vertex_shader_source = static const char *vertex_shader_source =
@ -31,18 +31,17 @@ static const char *fragment_shader_source =
// Vertices for a fullscreen quad // Vertices for a fullscreen quad
static const float vertices[] = { static const float vertices[] = {
// Position Texcoord // Position Texcoord
-1.0f, -1.0f, 0.0f, 1.0f, // Bottom left -1.0f, -1.0f, 0.0f, 1.0f, // bottom left
1.0f, -1.0f, 1.0f, 1.0f, // Bottom right 1.0f, -1.0f, 1.0f, 1.0f, // bottom right
1.0f, 1.0f, 1.0f, 0.0f, // Top right 1.0f, 1.0f, 1.0f, 0.0f, // top right
-1.0f, 1.0f, 0.0f, 0.0f, // Top left -1.0f, 1.0f, 0.0f, 0.0f, // top left
}; };
static const unsigned int indices[] = { static const unsigned int indices[] = {
0, 1, 2, // First triangle 0, 1, 2, // first triangle
2, 3, 0 // Second triangle 2, 3, 0 // second triangle
}; };
// Shader compilation helper
static GLuint compile_shader(GLenum type, const char *source) { static GLuint compile_shader(GLenum type, const char *source) {
GLuint shader = glCreateShader(type); GLuint shader = glCreateShader(type);
if (!shader) { if (!shader) {
@ -66,7 +65,6 @@ static GLuint compile_shader(GLenum type, const char *source) {
return shader; return shader;
} }
// Shader program creation helper
static GLuint create_shader_program(void) { static GLuint create_shader_program(void) {
GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source); GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source);
if (!vertex_shader) { if (!vertex_shader) {
@ -108,6 +106,145 @@ static GLuint create_shader_program(void) {
return program; return program;
} }
// Initialize OpenGL resources for output
static int init_gl_resources(chroma_output_t *output) {
if (!output || output->gl_resources_initialized) {
return CHROMA_OK;
}
// Create shader prog
output->shader_program = create_shader_program();
if (!output->shader_program) {
chroma_log("ERROR", "Failed to create shader program for output %u",
output->id);
return CHROMA_ERROR_EGL;
}
// Create and setup VBO/EBO
glGenBuffers(1, &output->vbo);
glGenBuffers(1, &output->ebo);
glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
GL_STATIC_DRAW);
output->texture_id = 0; // will be created when image is assigned
output->gl_resources_initialized = true;
chroma_log("DEBUG", "Initialized GL resources for output %u", output->id);
return CHROMA_OK;
}
// Create or update texture from image data
static int update_texture_from_image(chroma_output_t *output,
chroma_image_t *image) {
if (!output || !image || !image->loaded) {
return CHROMA_ERROR_INIT;
}
// If image data was already freed after previous GPU upload, we can't upload
// again
if (!image->data) {
chroma_log("ERROR",
"Cannot create texture: image data already freed for %s",
image->path);
return CHROMA_ERROR_IMAGE;
}
// Delete existing texture if it exists
if (output->texture_id != 0) {
glDeleteTextures(1, &output->texture_id);
output->texture_id = 0;
}
// Create new texture
glGenTextures(1, &output->texture_id);
glBindTexture(GL_TEXTURE_2D, output->texture_id);
// Set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Upload texture data (always RGBA now)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image->data);
glBindTexture(GL_TEXTURE_2D, 0);
// Mark this output as having uploaded its texture
output->texture_uploaded = true;
// Free system RAM copy only when ALL outputs using this image have uploaded
// to GPU
if (image->data) {
// Count total outputs using this image and how many have uploaded
int total_using = 0;
int uploaded_count = 0;
chroma_state_t *state = output->state;
for (int i = 0; i < state->output_count; i++) {
if (state->outputs[i].active && state->outputs[i].image == image) {
total_using++;
if (state->outputs[i].texture_uploaded) {
uploaded_count++;
}
}
}
// Only free image data when ALL outputs using it have uploaded
if (total_using > 0 && uploaded_count >= total_using) {
size_t freed_bytes =
(size_t)image->width * image->height * image->channels;
stbi_image_free(image->data);
image->data = NULL;
chroma_log("INFO",
"Freed %.2f MB of image data after all %d outputs uploaded to "
"GPU: %s",
(double)freed_bytes / (1024.0 * 1024.0), total_using,
image->path);
}
}
chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
image->width, image->height);
return CHROMA_OK;
}
// Cleanup OpenGL resources for output
static void cleanup_gl_resources(chroma_output_t *output) {
if (!output || !output->gl_resources_initialized) {
return;
}
if (output->texture_id != 0) {
glDeleteTextures(1, &output->texture_id);
output->texture_id = 0;
}
if (output->shader_program != 0) {
glDeleteProgram(output->shader_program);
output->shader_program = 0;
}
if (output->vbo != 0) {
glDeleteBuffers(1, &output->vbo);
output->vbo = 0;
}
if (output->ebo != 0) {
glDeleteBuffers(1, &output->ebo);
output->ebo = 0;
}
output->gl_resources_initialized = false;
chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id);
}
// EGL configuration selection // EGL configuration selection
static int choose_egl_config(EGLDisplay display, EGLConfig *config) { static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
EGLint attributes[] = {EGL_SURFACE_TYPE, EGLint attributes[] = {EGL_SURFACE_TYPE,
@ -292,6 +429,9 @@ void chroma_surface_destroy(chroma_output_t *output) {
return; return;
} }
// Clean up OpenGL resources first
cleanup_gl_resources(output);
if (output->egl_surface != EGL_NO_SURFACE) { if (output->egl_surface != EGL_NO_SURFACE) {
eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface); eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
output->egl_surface = EGL_NO_SURFACE; output->egl_surface = EGL_NO_SURFACE;
@ -315,33 +455,7 @@ void chroma_surface_destroy(chroma_output_t *output) {
chroma_log("DEBUG", "Destroyed surface for output %u", output->id); chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
} }
// Create texture from image data // Render wallpaper to output using cached resources
static GLuint create_texture_from_image(chroma_image_t *image) {
if (!image || !image->loaded || !image->data) {
return 0;
}
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// Set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Upload texture data
GLenum format = (image->channels == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, image->width, image->height, 0, format,
GL_UNSIGNED_BYTE, image->data);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
// Render wallpaper to output
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
if (!state || !output || !output->image || !output->image->loaded) { if (!state || !output || !output->image || !output->image->loaded) {
return CHROMA_ERROR_INIT; return CHROMA_ERROR_INIT;
@ -355,6 +469,17 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
return CHROMA_ERROR_EGL; return CHROMA_ERROR_EGL;
} }
if (init_gl_resources(output) != CHROMA_OK) {
return CHROMA_ERROR_EGL;
}
if (output->texture_id == 0) {
if (update_texture_from_image(output, output->image) != CHROMA_OK) {
chroma_log("ERROR", "Failed to update texture for output %u", output->id);
return CHROMA_ERROR_EGL;
}
}
// Set viewport // Set viewport
glViewport(0, 0, output->width, output->height); glViewport(0, 0, output->width, output->height);
@ -362,42 +487,21 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
// Create shader program (should be cached in real implementation) // Use cached shader program
GLuint program = create_shader_program(); glUseProgram(output->shader_program);
if (!program) {
return CHROMA_ERROR_EGL;
}
// Use shader program
glUseProgram(program);
// Create and bind texture
GLuint texture = create_texture_from_image(output->image);
if (!texture) {
chroma_log("ERROR", "Failed to create texture for output %u", output->id);
glDeleteProgram(program);
return CHROMA_ERROR_EGL;
}
// Bind cached texture
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture); glBindTexture(GL_TEXTURE_2D, output->texture_id);
glUniform1i(glGetUniformLocation(program, "texture"), 0); glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0);
// Create vertex buffer objects (should be cached in real implementation) // Use cached VBO/EBO
GLuint vbo, ebo; glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
glGenBuffers(1, &vbo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
glGenBuffers(1, &ebo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
GL_STATIC_DRAW);
// Set vertex attributes // Set vertex attributes
GLint position_attr = glGetAttribLocation(program, "position"); GLint position_attr = glGetAttribLocation(output->shader_program, "position");
GLint texcoord_attr = glGetAttribLocation(program, "texcoord"); GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");
glEnableVertexAttribArray(position_attr); glEnableVertexAttribArray(position_attr);
glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
@ -410,11 +514,11 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
// Draw // Draw
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// Cleanup // Unbind resources
glDeleteBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteBuffers(1, &ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDeleteTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, 0);
glDeleteProgram(program); glUseProgram(0);
// Swap buffers // Swap buffers
if (!eglSwapBuffers(state->egl_display, output->egl_surface)) { if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
@ -426,6 +530,21 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
// Commit surface // Commit surface
wl_surface_commit(output->surface); wl_surface_commit(output->surface);
chroma_log("DEBUG", "Rendered wallpaper to output %u", output->id); chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
output->id);
return CHROMA_OK; return CHROMA_OK;
} }
// Invalidate texture cache for output
void chroma_output_invalidate_texture(chroma_output_t *output) {
if (!output || !output->gl_resources_initialized) {
return;
}
if (output->texture_id != 0) {
glDeleteTextures(1, &output->texture_id);
output->texture_id = 0;
output->texture_uploaded = false; // reset upload flag
chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id);
}
}

24
systemd/chroma.service Normal file
View file

@ -0,0 +1,24 @@
[Unit]
Description=Chroma Wallpaper Daemon
After=graphical-session.target
Wants=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=chroma
ExecReload=kill -HUP $MAINPID
Restart=on-failure
RestartSec=1
KillMode=mixed
TimeoutStopSec=5
# Security
NoNewPrivileges=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=%h/.config/chroma
[Install]
WantedBy=multi-useer.target