/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package org.illarion.engine.graphic; import illarion.common.types.ServerCoordinate; import illarion.common.util.PoolThreadFactory; import illarion.common.util.Stoppable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Manager class that handles the light. It stores the pre-calculated light rays * as well as the light sources that are currently in use. Also it creates and * removes the light sources on request. * <p> * The whole calculations are threaded, so the light map that is the target of * all calculation results needs to be thread save. * </p> * * @author Nop * @author Martin Karing <nitram@illarion.org> */ public final class LightTracer implements Stoppable { private class CalculateLightTask implements Callable<Void> { @Nonnull private final LightSource light; private CalculateLightTask(@Nonnull LightSource light) { this.light = light; } @Override public Void call() throws Exception { if (isShutDown) { return null; } applyingLock.readLock().lock(); try { light.getCalculationLock().lock(); try { for (; ; ) { light.calculateShadows(); if (!light.isDirty()) { if (!lights.contains(light)) { lights.add(light); } light.setCalculating(false); break; } } } finally { light.getCalculationLock().unlock(); } } finally { applyingLock.readLock().unlock(); } notifyLightCalculationDone(); return null; } } @Nonnull private static final Logger log = LoggerFactory.getLogger(LightTracer.class); @Nonnull private final Callable<Void> publishLightsTask = () -> { notifyLightCalculationDone(); return null; }; /** * The executor service that takes care for calculating the lights. */ @Nonnull private final ExecutorService lightCalculationService; /** * This integer stores the amount of lights that are currently calculated. */ @Nonnull private final AtomicInteger lightsInProgress; /** * The lighting map that is the data source and the target for the light * calculating results for all light sources handled by this light tracer. */ @Nonnull private final LightingMap mapSource; /** * The list of lights that were processed at least once and contain all data to be applied to the map. */ @Nonnull private final List<LightSource> lights; /** * Is set true once the shutdown of the light tracer is triggered. */ private boolean isShutDown; @Nonnull private final ReadWriteLock applyingLock; /** * Default constructor of the light tracer. This tracer handles all light * sources that are on the map source that is set with the parameter. * * @param tracerMapSource the map the lights this tracer handles are on */ public LightTracer(@Nonnull LightingMap tracerMapSource) { mapSource = tracerMapSource; lights = new CopyOnWriteArrayList<>(); int maxThreads = Runtime.getRuntime().availableProcessors(); lightCalculationService = new ThreadPoolExecutor(0, maxThreads, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new PoolThreadFactory("LightTracer", true)); lightsInProgress = new AtomicInteger(0); applyingLock = new ReentrantReadWriteLock(); } /** * Add a light source to the list of light sources of this tracer. This * causes that this light source is taken into account and is rendered by * this light tracer if requested. * * @param light the light that shall be added to the light tracer and so to * the game screen */ public void addLight(@Nonnull LightSource light) { if (isShutDown) { return; } log.info("Adding new light to tracer: {}", light); light.setMapSource(mapSource); if (light.isDirty()) { if (!light.isCalculating()) { light.setCalculating(true); lightsInProgress.incrementAndGet(); lightCalculationService.submit(new CalculateLightTask(light)); } } else { if (!lights.contains(light)) { lights.add(light); } lightsInProgress.incrementAndGet(); lightCalculationService.submit(publishLightsTask); } } /** * Check if there are no lights set. * * @return true in case this tracer does not handle any lights currently */ public boolean isEmpty() { return lights.isEmpty() && (lightsInProgress.get() == 0); } /** * Notify the light system about a change on the map. This notify is * forwarded to all light sources and those only take the notify into * account in case its within the range of their rays. So every change on * the map should be reported to the tracer no matter if a light is around * this location or not. * * @param loc the location the change occurred at */ public void notifyChange(@Nonnull ServerCoordinate loc) { if (isShutDown) { return; } log.info("Got notification about change at {}", loc); for (LightSource light : lights) { light.notifyChange(loc); if (light.isDirty()) { log.trace("Light {} requires a update now.", light); refreshLight(light); } } } /** * Refresh the light tracer and force all lights to recalculate the values. */ public void refresh() { if (isShutDown) { return; } log.info("Refreshing all lights."); lights.forEach(this::refreshLight); } private void notifyLightCalculationDone() { if (lightsInProgress.decrementAndGet() == 0) { publishTidyLights(); } } public void updateLightLocation(@Nonnull LightSource light, @Nonnull ServerCoordinate newLocation) { if (isShutDown) { return; } log.info("Updating light {} location to: {}", light, newLocation); light.setLocation(newLocation); if (light.isDirty()) { refreshLight(light); } } /** * Move a light to the dirty lights list to have it updated at the next run. * * @param light the light that shall be updated. */ public void refreshLight(@Nonnull LightSource light) { if (isShutDown) { return; } log.info("Refreshing light {}", light); light.refresh(); if (light.getCalculationLock().tryLock()) { try { light.refresh(); addLight(light); } finally { light.getCalculationLock().unlock(); } } } public void replace(@Nonnull LightSource oldSource, @Nonnull LightSource newSource) { if (isShutDown) { return; } log.info("Replacing {} with {}", oldSource, newSource); oldSource.dispose(); addLight(newSource); } /** * Remove a light source from this tracer. This causes that the light is not * any longer calculated and rendered. * * @param light the light source that shall be removed */ public void remove(@Nonnull LightSource light) { if (isShutDown) { return; } log.info("Removing {} from tracer.", light); light.dispose(); lightsInProgress.incrementAndGet(); lightCalculationService.submit(publishLightsTask); } /** * Publish all tidy lights. */ private void publishTidyLights() { if (isShutDown) { return; } List<LightSource> disposedList = null; applyingLock.writeLock().lock(); try { log.info("Publishing lights now!"); for (LightSource light : lights) { if (light.isDisposed()) { if (disposedList == null) { disposedList = new ArrayList<>(); } disposedList.add(light); } else { light.apply(); } } } finally { applyingLock.writeLock().unlock(); } mapSource.renderLights(); if (disposedList != null) { lights.removeAll(disposedList); } } /** * Stop the thread as soon as possible. */ @Override public void saveShutdown() { log.info("LightTracer is shutting down now."); isShutDown = true; lightCalculationService.shutdown(); while (!lightCalculationService.isTerminated()) { try { lightCalculationService.awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) { // ignore } } lights.clear(); } }