├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── gradle.yml │ └── publish.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── doc ├── logo.png └── patreon.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── idea └── inspections.xml ├── settings.gradle └── src ├── .gitignore └── main ├── java └── net │ └── caffeinemc │ └── phosphor │ ├── common │ ├── block │ │ ├── BlockStateLightInfo.java │ │ └── BlockStateLightInfoAccess.java │ ├── chunk │ │ └── light │ │ │ ├── BlockLightStorageAccess.java │ │ │ ├── IReadonly.java │ │ │ ├── InitialLightingAccess.java │ │ │ ├── LevelPropagatorAccess.java │ │ │ ├── LightProviderUpdateTracker.java │ │ │ ├── LightStorageAccess.java │ │ │ ├── ServerLightingProviderAccess.java │ │ │ ├── SharedBlockLightData.java │ │ │ ├── SharedSkyLightData.java │ │ │ ├── SkyLightStorageAccess.java │ │ │ └── SkyLightStorageDataAccess.java │ └── util │ │ ├── LightUtil.java │ │ ├── chunk │ │ └── light │ │ │ ├── EmptyChunkNibbleArray.java │ │ │ ├── ReadonlyChunkNibbleArray.java │ │ │ └── SkyLightChunkNibbleArray.java │ │ ├── collections │ │ ├── DoubleBufferedLong2IntHashMap.java │ │ └── DoubleBufferedLong2ObjectHashMap.java │ │ └── math │ │ ├── ChunkSectionPosHelper.java │ │ └── DirectionHelper.java │ └── mixin │ ├── block │ ├── MixinAbstractBlockState.java │ └── MixinShapeCache.java │ ├── chunk │ ├── MixinChunkNibbleArray.java │ ├── MixinChunkStatus.java │ ├── MixinProtoChunk.java │ ├── MixinWorldChunk.java │ └── light │ │ ├── MixinBlockLightStorage.java │ │ ├── MixinBlockLightStorageData.java │ │ ├── MixinChunkBlockLightProvider.java │ │ ├── MixinChunkLightProvider.java │ │ ├── MixinChunkSkyLightProvider.java │ │ ├── MixinChunkToNibbleArrayMap.java │ │ ├── MixinLevelPropagator.java │ │ ├── MixinLightStorage.java │ │ ├── MixinLightingProvider.java │ │ ├── MixinServerLightingProvider.java │ │ ├── MixinSkyLightStorage.java │ │ └── MixinSkyLightStorageData.java │ └── world │ └── ThreadedAnvilChunkStorageAccess.java └── resources ├── assets └── phosphor │ └── icon.png ├── fabric.mod.json ├── phosphor.accesswidener └── phosphor.mixins.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = false 7 | max_line_length = 120 8 | tab_width = 4 9 | ij_continuation_indent_size = 8 10 | ij_formatter_off_tag = @formatter:off 11 | ij_formatter_on_tag = @formatter:on 12 | ij_formatter_tags_enabled = false 13 | ij_smart_tabs = false 14 | ij_wrap_on_typing = false 15 | 16 | [*.java] 17 | ij_java_align_consecutive_assignments = false 18 | ij_java_align_consecutive_variable_declarations = false 19 | ij_java_align_group_field_declarations = false 20 | ij_java_align_multiline_annotation_parameters = false 21 | ij_java_align_multiline_array_initializer_expression = false 22 | ij_java_align_multiline_assignment = false 23 | ij_java_align_multiline_binary_operation = false 24 | ij_java_align_multiline_chained_methods = false 25 | ij_java_align_multiline_extends_list = false 26 | ij_java_align_multiline_for = true 27 | ij_java_align_multiline_method_parentheses = false 28 | ij_java_align_multiline_parameters = true 29 | ij_java_align_multiline_parameters_in_calls = false 30 | ij_java_align_multiline_parenthesized_expression = false 31 | ij_java_align_multiline_resources = true 32 | ij_java_align_multiline_ternary_operation = false 33 | ij_java_align_multiline_text_blocks = false 34 | ij_java_align_multiline_throws_list = false 35 | ij_java_align_subsequent_simple_methods = false 36 | ij_java_align_throws_keyword = false 37 | ij_java_annotation_parameter_wrap = off 38 | ij_java_array_initializer_new_line_after_left_brace = false 39 | ij_java_array_initializer_right_brace_on_new_line = false 40 | ij_java_array_initializer_wrap = off 41 | ij_java_assert_statement_colon_on_next_line = false 42 | ij_java_assert_statement_wrap = off 43 | ij_java_assignment_wrap = off 44 | ij_java_binary_operation_sign_on_next_line = false 45 | ij_java_binary_operation_wrap = off 46 | ij_java_blank_lines_after_anonymous_class_header = 0 47 | ij_java_blank_lines_after_class_header = 0 48 | ij_java_blank_lines_after_imports = 1 49 | ij_java_blank_lines_after_package = 1 50 | ij_java_blank_lines_around_class = 1 51 | ij_java_blank_lines_around_field = 0 52 | ij_java_blank_lines_around_field_in_interface = 0 53 | ij_java_blank_lines_around_initializer = 1 54 | ij_java_blank_lines_around_method = 1 55 | ij_java_blank_lines_around_method_in_interface = 1 56 | ij_java_blank_lines_before_class_end = 0 57 | ij_java_blank_lines_before_imports = 1 58 | ij_java_blank_lines_before_method_body = 0 59 | ij_java_blank_lines_before_package = 0 60 | ij_java_block_brace_style = end_of_line 61 | ij_java_block_comment_at_first_column = true 62 | ij_java_call_parameters_new_line_after_left_paren = false 63 | ij_java_call_parameters_right_paren_on_new_line = false 64 | ij_java_call_parameters_wrap = off 65 | ij_java_case_statement_on_separate_line = true 66 | ij_java_catch_on_new_line = false 67 | ij_java_class_annotation_wrap = split_into_lines 68 | ij_java_class_brace_style = end_of_line 69 | ij_java_class_count_to_use_import_on_demand = 999 70 | ij_java_class_names_in_javadoc = 1 71 | ij_java_do_not_indent_top_level_class_members = false 72 | ij_java_do_not_wrap_after_single_annotation = false 73 | ij_java_do_while_brace_force = always 74 | ij_java_doc_add_blank_line_after_description = true 75 | ij_java_doc_add_blank_line_after_param_comments = false 76 | ij_java_doc_add_blank_line_after_return = false 77 | ij_java_doc_add_p_tag_on_empty_lines = true 78 | ij_java_doc_align_exception_comments = true 79 | ij_java_doc_align_param_comments = true 80 | ij_java_doc_do_not_wrap_if_one_line = false 81 | ij_java_doc_enable_formatting = true 82 | ij_java_doc_enable_leading_asterisks = true 83 | ij_java_doc_indent_on_continuation = false 84 | ij_java_doc_keep_empty_lines = true 85 | ij_java_doc_keep_empty_parameter_tag = true 86 | ij_java_doc_keep_empty_return_tag = true 87 | ij_java_doc_keep_empty_throws_tag = true 88 | ij_java_doc_keep_invalid_tags = true 89 | ij_java_doc_param_description_on_new_line = false 90 | ij_java_doc_preserve_line_breaks = false 91 | ij_java_doc_use_throws_not_exception_tag = true 92 | ij_java_else_on_new_line = false 93 | ij_java_enum_constants_wrap = off 94 | ij_java_extends_keyword_wrap = off 95 | ij_java_extends_list_wrap = off 96 | ij_java_field_annotation_wrap = split_into_lines 97 | ij_java_finally_on_new_line = false 98 | ij_java_for_brace_force = always 99 | ij_java_for_statement_new_line_after_left_paren = false 100 | ij_java_for_statement_right_paren_on_new_line = false 101 | ij_java_for_statement_wrap = off 102 | ij_java_generate_final_locals = false 103 | ij_java_generate_final_parameters = false 104 | ij_java_if_brace_force = always 105 | ij_java_imports_layout = *,|,javax.**,java.**,|,$* 106 | ij_java_indent_case_from_switch = true 107 | ij_java_insert_inner_class_imports = false 108 | ij_java_insert_override_annotation = true 109 | ij_java_keep_blank_lines_before_right_brace = 2 110 | ij_java_keep_blank_lines_between_package_declaration_and_header = 2 111 | ij_java_keep_blank_lines_in_code = 2 112 | ij_java_keep_blank_lines_in_declarations = 2 113 | ij_java_keep_control_statement_in_one_line = true 114 | ij_java_keep_first_column_comment = true 115 | ij_java_keep_indents_on_empty_lines = false 116 | ij_java_keep_line_breaks = true 117 | ij_java_keep_multiple_expressions_in_one_line = false 118 | ij_java_keep_simple_blocks_in_one_line = false 119 | ij_java_keep_simple_classes_in_one_line = false 120 | ij_java_keep_simple_lambdas_in_one_line = false 121 | ij_java_keep_simple_methods_in_one_line = false 122 | ij_java_label_indent_absolute = false 123 | ij_java_label_indent_size = 0 124 | ij_java_lambda_brace_style = end_of_line 125 | ij_java_layout_static_imports_separately = true 126 | ij_java_line_comment_add_space = false 127 | ij_java_line_comment_at_first_column = true 128 | ij_java_method_annotation_wrap = split_into_lines 129 | ij_java_method_brace_style = end_of_line 130 | ij_java_method_call_chain_wrap = off 131 | ij_java_method_parameters_new_line_after_left_paren = false 132 | ij_java_method_parameters_right_paren_on_new_line = false 133 | ij_java_method_parameters_wrap = off 134 | ij_java_modifier_list_wrap = false 135 | ij_java_names_count_to_use_import_on_demand = 999 136 | ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* 137 | ij_java_parameter_annotation_wrap = off 138 | ij_java_parentheses_expression_new_line_after_left_paren = false 139 | ij_java_parentheses_expression_right_paren_on_new_line = false 140 | ij_java_place_assignment_sign_on_next_line = false 141 | ij_java_prefer_longer_names = true 142 | ij_java_prefer_parameters_wrap = false 143 | ij_java_repeat_synchronized = true 144 | ij_java_replace_instanceof_and_cast = false 145 | ij_java_replace_null_check = true 146 | ij_java_replace_sum_lambda_with_method_ref = true 147 | ij_java_resource_list_new_line_after_left_paren = false 148 | ij_java_resource_list_right_paren_on_new_line = false 149 | ij_java_resource_list_wrap = off 150 | ij_java_space_after_closing_angle_bracket_in_type_argument = false 151 | ij_java_space_after_colon = true 152 | ij_java_space_after_comma = true 153 | ij_java_space_after_comma_in_type_arguments = true 154 | ij_java_space_after_for_semicolon = true 155 | ij_java_space_after_quest = true 156 | ij_java_space_after_type_cast = true 157 | ij_java_space_before_annotation_array_initializer_left_brace = false 158 | ij_java_space_before_annotation_parameter_list = false 159 | ij_java_space_before_array_initializer_left_brace = false 160 | ij_java_space_before_catch_keyword = true 161 | ij_java_space_before_catch_left_brace = true 162 | ij_java_space_before_catch_parentheses = true 163 | ij_java_space_before_class_left_brace = true 164 | ij_java_space_before_colon = true 165 | ij_java_space_before_colon_in_foreach = true 166 | ij_java_space_before_comma = false 167 | ij_java_space_before_do_left_brace = true 168 | ij_java_space_before_else_keyword = true 169 | ij_java_space_before_else_left_brace = true 170 | ij_java_space_before_finally_keyword = true 171 | ij_java_space_before_finally_left_brace = true 172 | ij_java_space_before_for_left_brace = true 173 | ij_java_space_before_for_parentheses = true 174 | ij_java_space_before_for_semicolon = false 175 | ij_java_space_before_if_left_brace = true 176 | ij_java_space_before_if_parentheses = true 177 | ij_java_space_before_method_call_parentheses = false 178 | ij_java_space_before_method_left_brace = true 179 | ij_java_space_before_method_parentheses = false 180 | ij_java_space_before_opening_angle_bracket_in_type_parameter = false 181 | ij_java_space_before_quest = true 182 | ij_java_space_before_switch_left_brace = true 183 | ij_java_space_before_switch_parentheses = true 184 | ij_java_space_before_synchronized_left_brace = true 185 | ij_java_space_before_synchronized_parentheses = true 186 | ij_java_space_before_try_left_brace = true 187 | ij_java_space_before_try_parentheses = true 188 | ij_java_space_before_type_parameter_list = false 189 | ij_java_space_before_while_keyword = true 190 | ij_java_space_before_while_left_brace = true 191 | ij_java_space_before_while_parentheses = true 192 | ij_java_space_inside_one_line_enum_braces = false 193 | ij_java_space_within_empty_array_initializer_braces = false 194 | ij_java_space_within_empty_method_call_parentheses = false 195 | ij_java_space_within_empty_method_parentheses = false 196 | ij_java_spaces_around_additive_operators = true 197 | ij_java_spaces_around_assignment_operators = true 198 | ij_java_spaces_around_bitwise_operators = true 199 | ij_java_spaces_around_equality_operators = true 200 | ij_java_spaces_around_lambda_arrow = true 201 | ij_java_spaces_around_logical_operators = true 202 | ij_java_spaces_around_method_ref_dbl_colon = false 203 | ij_java_spaces_around_multiplicative_operators = true 204 | ij_java_spaces_around_relational_operators = true 205 | ij_java_spaces_around_shift_operators = true 206 | ij_java_spaces_around_type_bounds_in_type_parameters = true 207 | ij_java_spaces_around_unary_operator = false 208 | ij_java_spaces_within_angle_brackets = false 209 | ij_java_spaces_within_annotation_parentheses = false 210 | ij_java_spaces_within_array_initializer_braces = false 211 | ij_java_spaces_within_braces = false 212 | ij_java_spaces_within_brackets = false 213 | ij_java_spaces_within_cast_parentheses = false 214 | ij_java_spaces_within_catch_parentheses = false 215 | ij_java_spaces_within_for_parentheses = false 216 | ij_java_spaces_within_if_parentheses = false 217 | ij_java_spaces_within_method_call_parentheses = false 218 | ij_java_spaces_within_method_parentheses = false 219 | ij_java_spaces_within_parentheses = false 220 | ij_java_spaces_within_switch_parentheses = false 221 | ij_java_spaces_within_synchronized_parentheses = false 222 | ij_java_spaces_within_try_parentheses = false 223 | ij_java_spaces_within_while_parentheses = false 224 | ij_java_special_else_if_treatment = true 225 | ij_java_subclass_name_suffix = Impl 226 | ij_java_ternary_operation_signs_on_next_line = false 227 | ij_java_ternary_operation_wrap = off 228 | ij_java_test_name_suffix = Test 229 | ij_java_throws_keyword_wrap = off 230 | ij_java_throws_list_wrap = off 231 | ij_java_use_external_annotations = false 232 | ij_java_use_fq_class_names = false 233 | ij_java_use_relative_indents = false 234 | ij_java_use_single_class_imports = true 235 | ij_java_variable_annotation_wrap = off 236 | ij_java_visibility = public 237 | ij_java_while_brace_force = always 238 | ij_java_while_on_new_line = false 239 | ij_java_wrap_comments = false 240 | ij_java_wrap_first_method_in_call_chain = false 241 | ij_java_wrap_long_lines = false 242 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png binary -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jellysquid3 4 | patreon: jellysquid 5 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: gradle-ci 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | - name: Set up JDK 13 | uses: actions/setup-java@v2 14 | with: 15 | distribution: 'adopt' 16 | java-version: 17 17 | - name: Set release tag 18 | if: startsWith(github.ref, 'refs/tags/') 19 | run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 20 | - name: Build with Gradle 21 | run: ./gradlew build 22 | env: 23 | BUILD_ID: ${{ github.run_number }} 24 | - name: Upload prod artifacts 25 | uses: actions/upload-artifact@v3 26 | with: 27 | name: build-artifacts 28 | path: | 29 | build/libs/ 30 | !build/libs/*-sources.jar 31 | - name: Upload dev artifacts 32 | uses: actions/upload-artifact@v3 33 | with: 34 | name: build-artifacts 35 | path: build/devlibs/ 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: release-artifacts 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout sources 12 | uses: actions/checkout@v2 13 | - name: Set up JDK 14 | uses: actions/setup-java@v2 15 | with: 16 | distribution: 'adopt' 17 | java-version: 17 18 | - name: Set release tag 19 | if: startsWith(github.ref, 'refs/tags/') 20 | run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 21 | - name: Set build id 22 | if: github.event.release.prerelease 23 | run: echo "BUILD_ID=SNAPSHOT" >> $GITHUB_ENV 24 | - name: Build with Gradle 25 | run: ./gradlew build 26 | - name: Upload assets to GitHub 27 | uses: AButler/upload-release-assets@v2.0 28 | with: 29 | files: 'build/libs/*;!build/libs/*-sources.jar;build/devlibs/*' 30 | repo-token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .settings 5 | .metadata 6 | .classpath 7 | .project 8 | 9 | # idea 10 | out 11 | *.ipr 12 | *.iws 13 | *.iml 14 | .idea 15 | classes 16 | 17 | # gradle 18 | build 19 | .gradle 20 | 21 | # other 22 | eclipse 23 | run 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Issues 2 | 3 | When opening issues, please be sure to include the following information as applicable. 4 | 5 | - The exact version of the mod you are running, such as `0.1.0-fabric`, and the version of Fabric you are using. 6 | - If your issue is a crash, attach the latest client or server log and the complete crash report as a file. You can 7 | attach these as a file (preferred) or host them on a service such as [GitHub Gist](https://gist.github.com/) or [Hastebin](https://hastebin.com/). 8 | - If your issue is a bug or otherwise unexpected behavior, explain what you expected to happen. 9 | - If your issue only occurs with other mods installed, be sure to specify the names and versions of those mods. 10 | 11 | ## Pull Requests 12 | 13 | It's super awesome to hear you're wishing to contribute to the project! Before you open a pull request, you'll need to 14 | give a quick read to the following guidelines. 15 | 16 | ### Contributor License Agreement (CLA) 17 | 18 | By submitting changes to this repository, you are hereby agreeing that: 19 | 20 | - Your contributions will be licensed irrecoverably under the [GNU LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.html). 21 | - Your contributions are of your own work and free of legal restrictions (such as patents and copyrights) or other 22 | issues which would pose issues for inclusion or distribution under the above license. 23 | 24 | If you do not agree to these terms, please do not submit contributions to this repository. If you have any questions 25 | about these terms, feel free to get in contact with me through the [public Discord server](https://caffeinemc.net/discord) or 26 | through opening an issue. 27 | 28 | ### Code Style 29 | 30 | When contributing source code changes to the project, ensure that you make consistent use of the code style guidelines 31 | used throughout the codebase (which follow pretty closely after the standard Java code style guidelines). These guidelines 32 | have also been packaged as EditorConfig and IDEA inspection profiles which can be found in the repository root and `idea` 33 | directory respectively. 34 | 35 | - Use 4 spaces for indentation, not tabs. Avoid lines which exceed 120 characters. 36 | - Use `this` to qualify member and field access. 37 | - Always use braces when writing if-statements and loops. 38 | - Annotate overriding methods with `@Override` so that breaking changes when updating will create hard compile errors. 39 | - Comment code which needs to mimic vanilla behavior with `[VanillaCopy]` so it can be inspected when updating. 40 | 41 | ### Making a Pull Request 42 | 43 | Your pull request should include a brief description of the changes it makes and link to any open issues which it 44 | resolves. You should also ensure that your code is well documented where non-trivial and that it follows the 45 | outlined code style guidelines above. 46 | 47 | If you're adding new Mixin patches to the project, please ensure that you have created appropriate entries to disable 48 | them in the config file. Mixins should always be self-contained and grouped into "patch sets" which are easy to isolate. 49 | 50 | Additionally, if you're making changes for the sake of improving performance in either the vanilla game or the project 51 | itself, try to provide a detailed test-case and benchmark for them. It's understandable that micro-benchmarking is 52 | difficult in the context of Minecraft, but even naive figures taken from a profiler, timings graph, or a simple counter 53 | will be greatly appreciated and help track incremental improvements. 54 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Phosphor (for Fabric) 4 | ![GitHub license](https://img.shields.io/github/license/CaffeineMC/phosphor-fabric.svg) 5 | ![GitHub issues](https://img.shields.io/github/issues/CaffeineMC/phosphor-fabric.svg) 6 | ![GitHub tag](https://img.shields.io/github/v/tag/CaffeineMC/phosphor-fabric.svg) 7 | 8 | Phosphor is a free and open-source Minecraft mod (under GNU GPLv3) aiming to save your CPU cycles and improve performance by optimizing one of Minecraft's most inefficient areas-- the lighting engine. 9 | It works on **both the client and server**, and can be installed on servers **without requiring clients to also have the mod**. 10 | 11 | The mod is designed to be as minimal as possible in the changes it makes, and as such, does not modify the light model or interfaces of vanilla Minecraft. Because of this, Phosphor should be compatible 12 | with many Minecraft mods (so long as they do not make drastic changes to how the lighting engine works.) If you've ran into a compatibility problem, please open an issue! 13 | 14 | 15 | --- 16 | 17 | ## Installation 18 | 19 | ### Manual installation (recommended) 20 | 21 | You will need Fabric Loader 0.10.x or newer installed in your game in order to load Phosphor. If you haven't installed 22 | Fabric mods before, you can find a variety of community guides for doing so [here](https://fabricmc.net/wiki/install). 23 | 24 | #### Stable releases 25 | 26 | ![GitHub release](https://img.shields.io/github/release/CaffeineMC/phosphor-fabric.svg) 27 | 28 | The latest releases of Phosphor are published to our [Modrinth](https://modrinth.com/mod/phosphor) and 29 | [GitHub release](https://github.com/CaffeineMC/phosphor-fabric/releases) pages. Releases are considered by our team to be 30 | **suitable for general use**, but they are not guaranteed to be free of bugs and other issues. 31 | 32 | Usually, releases will be made available on GitHub slightly sooner than other locations. 33 | 34 | #### Bleeding-edge builds (unstable) 35 | 36 | [![GitHub build status](https://img.shields.io/github/workflow/status/CaffeineMC/phosphor-fabric/gradle-ci/1.16.x/dev)](https://github.com/CaffeineMC/phosphor-fabric/actions/workflows/gradle.yml) 37 | 38 | If you are a player who is looking to get your hands on the latest **bleeding-edge changes for testing**, consider 39 | taking a look at the automated builds produced through our [GitHub Actions workflow](https://github.com/CaffeineMC/phosphor-fabric/actions/workflows/gradle.yml?query=event%3Apush). 40 | This workflow automatically runs every time a change is pushed to the repository, and as such, the builds it produces 41 | will generally reflect the latest snapshot of development. 42 | 43 | Bleeding edge builds will often include unfinished code that hasn't been extensively tested. That code may introduce 44 | incomplete features, bugs, crashes, and all other kinds of weird issues. You **should not use these bleeding edge builds** 45 | unless you know what you are doing and are comfortable with software debugging. If you report issues using these builds, 46 | we will expect that this is the case. Caveat emptor. 47 | 48 | ### CurseForge 49 | 50 | [![CurseForge downloads](http://cf.way2muchnoise.eu/full_372124_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/phosphor) 51 | 52 | If you are using the CurseForge client, you can continue to find downloads through our 53 | [CurseForge page](https://www.curseforge.com/minecraft/mc-mods/phosphor). Unless you are using the CurseForge 54 | client, you should prefer the downloads linked on our Modrinth or GitHub release pages above. 55 | 56 | --- 57 | 58 | ### Reporting Issues 59 | 60 | You can report bugs and crashes by opening an issue on our [issue tracker](https://github.com/CaffeineMC/phosphor-fabric/issues). 61 | Before opening a new issue, use the search tool to make sure that your issue has not already been reported and ensure 62 | that you have completely filled out the issue template. Issues which are duplicates or do not contain the necessary 63 | information to triage and debug may be closed. 64 | 65 | Please note that while the issue tracker is open to feature requests, development is primarily focused on 66 | improving hardware compatibility, performance, and finishing any unimplemented features necessary for parity with 67 | the vanilla renderer. 68 | 69 | ### Community 70 | [![Discord chat](https://img.shields.io/badge/chat%20on-discord-7289DA?logo=discord&logoColor=white)](https://caffeinemc.net/discord) 71 | 72 | We have an [official Discord community](https://caffeinemc.net/discord) for all of our projects. By joining, you can: 73 | - Get installation help and technical support with all of our mods 74 | - Be notified of the latest developments as they happen 75 | - Get involved and collaborate with the rest of our team 76 | - ... and just hang out with the rest of our community. 77 | 78 | --- 79 | 80 | ### Building from sources 81 | 82 | Support is not provided for setting up build environments or compiling the mod. We ask that 83 | users who are looking to get their hands dirty with the code have a basic understanding of compiling Java/Gradle 84 | projects. The basic overview is provided here for those familiar. 85 | 86 | #### Requirements 87 | 88 | - JDK 8 or newer 89 | - You will need JDK 8 in order to build Phosphor, which can be installed through a supported package manager such as 90 | [Chocolatey](https://chocolatey.org/) on Windows or [SDKMAN!](https://sdkman.io/) on other platforms. If you'd prefer to 91 | not use a package manager, you can always grab the installers or packages directly from [Adoptium](https://adoptium.net/). 92 | - Gradle 6.7 or newer (optional) 93 | - The [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:using_wrapper) is provided in 94 | this repository can be used instead of installing a suitable version of Gradle yourself. However, if you are building 95 | many projects, you may prefer to install it yourself through a suitable package manager as to save disk space and to 96 | avoid many different Gradle daemons sitting around in memory. 97 | 98 | #### Building with Gradle 99 | 100 | Phosphor uses a typical Gradle project structure and can be built by simply running the default `build` task. After Gradle 101 | finishes building the project, you can find the build artifacts (typical mod binaries, and their sources) in 102 | `build/libs`. 103 | 104 | **Tip:** If this is a one-off build, and you would prefer the Gradle daemon does not stick around in memory afterwards, 105 | try adding the [`--no-daemon` flag](https://docs.gradle.org/current/userguide/gradle_daemon.html#sec:disabling_the_daemon) 106 | to ensure that the daemon is torn down after the build is complete. However, subsequent builds of the project will 107 | [start more slowly](https://docs.gradle.org/current/userguide/gradle_daemon.html#sec:why_the_daemon) if the Gradle 108 | daemon is not available to be re-used. 109 | 110 | 111 | Build artifacts ending in `dev` are outputs containing the sources and compiled classes 112 | before they are remapped into stable intermediary names. If you are working in a developer environment and would 113 | like to add the mod to your game, you should prefer to use the `modRuntime` or `modImplementation` configurations provided by 114 | Loom instead of these outputs. 115 | 116 | --- 117 | 118 | ### License 119 | 120 | Phosphor is licensed under GNU LGPLv3, a free and open-source license. For more information, please see the [license file](LICENSE.txt). 121 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '0.12-SNAPSHOT' 3 | } 4 | 5 | sourceCompatibility = JavaVersion.VERSION_17 6 | targetCompatibility = JavaVersion.VERSION_17 7 | 8 | archivesBaseName = project.archives_base_name 9 | group = project.maven_group 10 | 11 | def release_tag = System.getenv("RELEASE_TAG") 12 | def build_id = System.getenv("BUILD_ID") 13 | 14 | version = release_tag == null ? "mc${project.minecraft_version}-${project.mod_version}" : release_tag 15 | 16 | if (build_id != null) { 17 | version += build_id == 'SNAPSHOT' ? '-SNAPSHOT' : "+build.${build_id}" 18 | } 19 | 20 | loom { 21 | accessWidenerPath = file("src/main/resources/phosphor.accesswidener") 22 | 23 | mixin { 24 | defaultRefmapName = "mixins.phosphor.refmap.json" 25 | } 26 | } 27 | 28 | dependencies { 29 | //to change the versions see the gradle.properties file 30 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 31 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 32 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 33 | } 34 | 35 | processResources { 36 | inputs.property "version", project.mod_version 37 | 38 | filesMatching("fabric.mod.json") { 39 | expand "version": project.mod_version 40 | } 41 | } 42 | 43 | tasks.withType(JavaCompile).configureEach { 44 | // ensure that the encoding is set to UTF-8, no matter what the system default is 45 | // this fixes some edge cases with special characters not displaying correctly 46 | // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html 47 | // If Javadoc is generated, this must be specified in that task too. 48 | it.options.encoding = "UTF-8" 49 | 50 | // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. 51 | it.options.release = 17 52 | } 53 | 54 | java { 55 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 56 | // if it is present. 57 | // If you remove this line, sources will not be generated. 58 | withSourcesJar() 59 | } 60 | 61 | jar { 62 | from "LICENSE.txt" 63 | } 64 | -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaffeineMC/phosphor-fabric/4e8ed36936e6cef3264f884f40827af605aa389a/doc/logo.png -------------------------------------------------------------------------------- /doc/patreon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaffeineMC/phosphor-fabric/4e8ed36936e6cef3264f884f40827af605aa389a/doc/patreon.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | # check these on https://fabricmc.net/versions.html 6 | minecraft_version=1.19 7 | yarn_mappings=1.19+build.1 8 | loader_version=0.14.6 9 | 10 | # Mod Properties 11 | mod_version=0.8.1 12 | maven_group=net.caffeinemc 13 | archives_base_name=phosphor-fabric 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaffeineMC/phosphor-fabric/4e8ed36936e6cef3264f884f40827af605aa389a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaffeineMC/phosphor-fabric/4e8ed36936e6cef3264f884f40827af605aa389a/gradlew -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /idea/inspections.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | jmh -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/block/BlockStateLightInfo.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.block; 2 | 3 | import net.minecraft.util.shape.VoxelShape; 4 | 5 | public interface BlockStateLightInfo { 6 | VoxelShape[] getExtrudedFaces(); 7 | 8 | int getLightSubtracted(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/block/BlockStateLightInfoAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.block; 2 | 3 | public interface BlockStateLightInfoAccess { 4 | BlockStateLightInfo getLightInfo(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/BlockLightStorageAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | public interface BlockLightStorageAccess extends LightStorageAccess { 4 | boolean isLightEnabled(long sectionPos); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/IReadonly.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | public interface IReadonly { 4 | boolean isReadonly(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/InitialLightingAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | public interface InitialLightingAccess { 4 | void enableSourceLight(long chunkPos); 5 | 6 | void enableLightUpdates(long chunkPos); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/LevelPropagatorAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | public interface LevelPropagatorAccess { 4 | void invokePropagateLevel(long sourceId, long targetId, int level, boolean decrease); 5 | 6 | void propagateLevel(long sourceId, long targetId, boolean decrease); 7 | 8 | void checkForUpdates(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/LightProviderUpdateTracker.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | public interface LightProviderUpdateTracker { 4 | /** 5 | * Discards all pending updates for the specified chunk section. 6 | */ 7 | void cancelUpdatesForChunk(long sectionPos); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/LightStorageAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | import net.minecraft.world.chunk.light.ChunkLightProvider; 4 | 5 | public interface LightStorageAccess { 6 | boolean callHasSection(long sectionPos); 7 | 8 | /** 9 | * Runs scheduled cleanups of light data 10 | */ 11 | void runCleanups(); 12 | 13 | void enableLightUpdates(long chunkPos); 14 | 15 | /** 16 | * Disables light updates and source light for the provided chunkPos and additionally removes all light data associated to the chunk. 17 | */ 18 | void disableChunkLight(long chunkPos, ChunkLightProvider lightProvider); 19 | 20 | void invokeSetColumnEnabled(long chunkPos, boolean enabled); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/ServerLightingProviderAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import net.minecraft.world.chunk.Chunk; 6 | 7 | public interface ServerLightingProviderAccess { 8 | CompletableFuture setupLightmaps(Chunk chunk); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/SharedBlockLightData.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.util.collections.DoubleBufferedLong2ObjectHashMap; 4 | import net.minecraft.world.chunk.ChunkNibbleArray; 5 | 6 | public interface SharedBlockLightData { 7 | /** 8 | * Make this instance a copy of another. The shared copy cannot be directly written into. 9 | * 10 | * @param queue The queue of light updates 11 | */ 12 | void makeSharedCopy(DoubleBufferedLong2ObjectHashMap queue); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/SharedSkyLightData.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.util.collections.DoubleBufferedLong2IntHashMap; 4 | import net.caffeinemc.phosphor.common.util.collections.DoubleBufferedLong2ObjectHashMap; 5 | import net.minecraft.world.chunk.ChunkNibbleArray; 6 | 7 | public interface SharedSkyLightData { 8 | /** 9 | * Make this instance a copy of another. The shared copy cannot be directly written into. 10 | * 11 | * @param queue The queue of light updates 12 | * @param topSectionQueue The queue of top sections 13 | */ 14 | void makeSharedCopy(DoubleBufferedLong2ObjectHashMap queue, DoubleBufferedLong2IntHashMap topSectionQueue); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/SkyLightStorageAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | public interface SkyLightStorageAccess extends LightStorageAccess { 4 | boolean callIsAboveMinHeight(int y); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/chunk/light/SkyLightStorageDataAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.chunk.light; 2 | 3 | public interface SkyLightStorageDataAccess { 4 | /** 5 | * Bridge method to SkyLightStorageData#defaultHeight(). 6 | */ 7 | int getDefaultHeight(); 8 | 9 | /** 10 | * Returns the height map value for the given block column in the world. 11 | */ 12 | int getHeight(long pos); 13 | 14 | void updateMinHeight(int y); 15 | 16 | void setHeight(long chunkPos, int y); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/LightUtil.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util; 2 | 3 | import net.minecraft.util.function.BooleanBiFunction; 4 | import net.minecraft.util.shape.VoxelShape; 5 | import net.minecraft.util.shape.VoxelShapes; 6 | 7 | public class LightUtil { 8 | /** 9 | * Replacement for {@link VoxelShapes#unionCoversFullCube(VoxelShape, VoxelShape)}. This implementation early-exits 10 | * in some common situations to avoid unnecessary computation. 11 | * 12 | * @author JellySquid 13 | */ 14 | public static boolean unionCoversFullCube(VoxelShape a, VoxelShape b) { 15 | // At least one shape is a full cube and will match 16 | if (a == VoxelShapes.fullCube() || b == VoxelShapes.fullCube()) { 17 | return true; 18 | } 19 | 20 | boolean ae = a.isEmpty(); 21 | boolean be = b.isEmpty(); 22 | 23 | // If both shapes are empty, they can never overlap a full cube 24 | if (ae && be) { 25 | return false; 26 | } 27 | 28 | // If both shapes are the same, it is pointless to merge them 29 | if (a == b) { 30 | return coversFullCube(a); 31 | } 32 | 33 | // If one of the shapes is empty, we can skip merging as any shape merged with an empty shape is the same shape 34 | if (ae || be) { 35 | return coversFullCube(ae ? b : a); 36 | } 37 | 38 | // No special optimizations can be performed, so we need to merge both shapes and test them 39 | return coversFullCube(VoxelShapes.combine(a, b, BooleanBiFunction.OR)); 40 | } 41 | 42 | private static boolean coversFullCube(VoxelShape shape) { 43 | return !VoxelShapes.matchesAnywhere(VoxelShapes.fullCube(), shape, BooleanBiFunction.ONLY_FIRST); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/chunk/light/EmptyChunkNibbleArray.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util.chunk.light; 2 | 3 | public class EmptyChunkNibbleArray extends ReadonlyChunkNibbleArray { 4 | public EmptyChunkNibbleArray() { 5 | } 6 | 7 | @Override 8 | public byte[] asByteArray() { 9 | return new byte[2048]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/chunk/light/ReadonlyChunkNibbleArray.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.chunk.light.IReadonly; 4 | import net.minecraft.world.chunk.ChunkNibbleArray; 5 | 6 | public class ReadonlyChunkNibbleArray extends ChunkNibbleArray implements IReadonly { 7 | public ReadonlyChunkNibbleArray() { 8 | } 9 | 10 | public ReadonlyChunkNibbleArray(byte[] bs) { 11 | super(bs); 12 | } 13 | 14 | @Override 15 | public ChunkNibbleArray copy() { 16 | return new ChunkNibbleArray(this.asByteArray()); 17 | } 18 | 19 | @Override 20 | public boolean isReadonly() { 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/chunk/light/SkyLightChunkNibbleArray.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util.chunk.light; 2 | 3 | import net.minecraft.world.chunk.ChunkNibbleArray; 4 | 5 | public class SkyLightChunkNibbleArray extends ReadonlyChunkNibbleArray { 6 | public SkyLightChunkNibbleArray(final byte[] inheritedLightmap) { 7 | super(inheritedLightmap); 8 | } 9 | 10 | public SkyLightChunkNibbleArray(final ChunkNibbleArray inheritedLightmap) { 11 | this(inheritedLightmap.asByteArray()); 12 | } 13 | 14 | @Override 15 | protected int get(final int index) { 16 | return super.get(index & 255); 17 | } 18 | 19 | @Override 20 | public byte[] asByteArray() { 21 | byte[] byteArray = new byte[2048]; 22 | 23 | for(int i = 0; i < 16; ++i) { 24 | System.arraycopy(this.bytes, 0, byteArray, i * 128, 128); 25 | } 26 | 27 | return byteArray; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/collections/DoubleBufferedLong2IntHashMap.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util.collections; 2 | 3 | import it.unimi.dsi.fastutil.Hash; 4 | import it.unimi.dsi.fastutil.longs.*; 5 | 6 | import java.util.concurrent.locks.StampedLock; 7 | 8 | /** 9 | * A double buffered Long->Object hash table which allows for multiple readers to see a consistent view without 10 | * contention over shared resources. The synchronous (owned) view must be synced using 11 | * {@link DoubleBufferedLong2IntHashMap#flushChangesSync()} after all desired changes have been made. 12 | * 13 | * Methods labeled as synchronous access the owned mutable view of this map which behaves as the back-buffer. When 14 | * changes are flushed, the front-buffer is locked, written into, and then unlocked. The locking implementation is 15 | * optimized for (relatively) infrequent flips. 16 | * 17 | * {@link Integer#MIN_VALUE} is used to indicate values which are to be removed and cannot be added to the queue. 18 | */ 19 | public class DoubleBufferedLong2IntHashMap { 20 | // The map of pending entry updates to be applied to the visible hash table 21 | private final Long2IntMap mapPending; 22 | 23 | // The hash table of entries belonging to the owning thread 24 | private Long2IntMap mapLocal; 25 | 26 | // The hash table of entries available to other threads 27 | private Long2IntMap mapShared; 28 | 29 | // The lock used by other threads to grab values from the visible map asynchronously. This prevents other threads 30 | // from seeing partial updates while the changes are flushed. The lock implementation is specially selected to 31 | // optimize for the common case: infrequent writes, very frequent reads. 32 | private final StampedLock lock = new StampedLock(); 33 | 34 | public DoubleBufferedLong2IntHashMap() { 35 | this(16, Hash.FAST_LOAD_FACTOR); 36 | } 37 | 38 | public DoubleBufferedLong2IntHashMap(int capacity, float loadFactor) { 39 | this.mapLocal = new Long2IntOpenHashMap(capacity, loadFactor); 40 | this.mapShared = new Long2IntOpenHashMap(capacity, loadFactor); 41 | this.mapPending = new Long2IntOpenHashMap(capacity, loadFactor); 42 | } 43 | 44 | public void defaultReturnValueSync(int v) { 45 | this.mapLocal.defaultReturnValue(v); 46 | } 47 | 48 | public int putSync(long k, int v) { 49 | if (v == Integer.MIN_VALUE) { 50 | throw new IllegalArgumentException("Value Integer.MIN_VALUE cannot be used"); 51 | } 52 | 53 | this.mapPending.put(k, v); 54 | 55 | return this.mapLocal.put(k, v); 56 | } 57 | 58 | public int removeSync(long k) { 59 | this.mapPending.put(k, Integer.MIN_VALUE); 60 | 61 | return this.mapLocal.remove(k); 62 | } 63 | 64 | public int getSync(long k) { 65 | return this.mapLocal.get(k); 66 | } 67 | 68 | public int getAsync(long k) { 69 | long stamp; 70 | int ret = Integer.MIN_VALUE; 71 | 72 | do { 73 | stamp = this.lock.tryOptimisticRead(); 74 | 75 | try { 76 | ret = this.mapShared.get(k); 77 | } catch (ArrayIndexOutOfBoundsException ignored) { } // Swallow memory errors on failed optimistic reads 78 | } while (!this.lock.validate(stamp)); 79 | 80 | return ret; 81 | } 82 | 83 | /** 84 | * Flushes all pending changes to the visible hash table seen by outside consumers. 85 | */ 86 | public void flushChangesSync() { 87 | // Early-exit if there's no work to do 88 | if (this.mapPending.isEmpty()) { 89 | return; 90 | } 91 | 92 | // Swap the local and shared tables immediately, and then block the writer thread while we finish copying 93 | this.swapTables(); 94 | 95 | // Use a non-allocating iterator if possible, otherwise we're going to hurt 96 | for (Long2IntMap.Entry entry : Long2IntMaps.fastIterable(this.mapPending)) { 97 | final long key = entry.getLongKey(); 98 | final int val = entry.getIntValue(); 99 | 100 | // MIN_VALUE indicates that the value should be removed instead 101 | if (val == Integer.MIN_VALUE) { 102 | this.mapLocal.remove(key); 103 | } else { 104 | this.mapLocal.put(key, val); 105 | } 106 | } 107 | 108 | this.mapLocal.defaultReturnValue(this.mapShared.defaultReturnValue()); 109 | 110 | this.mapPending.clear(); 111 | } 112 | 113 | private void swapTables() { 114 | final long writeLock = this.lock.writeLock(); 115 | 116 | Long2IntMap mapShared = this.mapLocal; 117 | Long2IntMap mapLocal = this.mapShared; 118 | 119 | this.mapShared = mapShared; 120 | this.mapLocal = mapLocal; 121 | 122 | this.lock.unlockWrite(writeLock); 123 | } 124 | 125 | public Long2IntOpenHashMap createSyncView() { 126 | return new Long2IntOpenHashMap() { 127 | @Override 128 | public int size() { 129 | return DoubleBufferedLong2IntHashMap.this.mapLocal.size(); 130 | } 131 | 132 | @Override 133 | public void defaultReturnValue(int rv) { 134 | DoubleBufferedLong2IntHashMap.this.defaultReturnValueSync(rv); 135 | } 136 | 137 | @Override 138 | public int defaultReturnValue() { 139 | return DoubleBufferedLong2IntHashMap.this.mapLocal.defaultReturnValue(); 140 | } 141 | 142 | @Override 143 | public boolean containsKey(long key) { 144 | return DoubleBufferedLong2IntHashMap.this.mapLocal.containsKey(key); 145 | } 146 | 147 | @Override 148 | public boolean containsValue(int value) { 149 | return DoubleBufferedLong2IntHashMap.this.mapLocal.containsValue(value); 150 | } 151 | 152 | @Override 153 | public int get(long key) { 154 | return DoubleBufferedLong2IntHashMap.this.getSync(key); 155 | } 156 | 157 | @Override 158 | public int put(long key, int value) { 159 | return DoubleBufferedLong2IntHashMap.this.putSync(key, value); 160 | } 161 | 162 | @Override 163 | public int remove(long key) { 164 | return DoubleBufferedLong2IntHashMap.this.removeSync(key); 165 | } 166 | 167 | @Override 168 | public boolean isEmpty() { 169 | return DoubleBufferedLong2IntHashMap.this.mapLocal.isEmpty(); 170 | } 171 | }; 172 | } 173 | } -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/collections/DoubleBufferedLong2ObjectHashMap.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util.collections; 2 | 3 | import it.unimi.dsi.fastutil.Hash; 4 | import it.unimi.dsi.fastutil.longs.Long2ObjectMap; 5 | import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; 6 | import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; 7 | 8 | import java.util.concurrent.locks.StampedLock; 9 | 10 | /** 11 | * A double buffered Long->Object hash table which allows for multiple readers to see a consistent view without\ 12 | * contention over shared resources. The synchronous (owned) view must be synced using 13 | * {@link DoubleBufferedLong2IntHashMap#flushChangesSync()} after all desired changes have been made. 14 | * 15 | * Methods labeled as synchronous access the owned mutable view of this map which behaves as the back-buffer. When 16 | * changes are flushed, the front-buffer is swapped with the back-buffer, and any pending changes are written into the 17 | * new back-buffer. The locking implementation is optimized for (relatively) infrequent flips. 18 | * 19 | * Null is used to indicate a value to be removed, and as such, cannot be used as a value type in the collection. If 20 | * you need to remove an element, use {@link DoubleBufferedLong2IntHashMap#removeSync(long)}. 21 | */ 22 | public class DoubleBufferedLong2ObjectHashMap { 23 | // The map of pending entry updates to be applied to the visible hash table 24 | private final Long2ObjectMap mapPending; 25 | 26 | // The hash table of entries belonging to the owning thread 27 | private Long2ObjectMap mapLocal; 28 | 29 | // The hash table of entries available to other threads 30 | private Long2ObjectMap mapShared; 31 | 32 | // The lock used by other threads to grab values from the visible map asynchronously. This prevents other threads 33 | // from seeing partial updates while the changes are flushed. The lock implementation is specially selected to 34 | // optimize for the common case: infrequent writes, very frequent reads. 35 | private final StampedLock lock = new StampedLock(); 36 | 37 | public DoubleBufferedLong2ObjectHashMap() { 38 | this(16, Hash.FAST_LOAD_FACTOR); 39 | } 40 | 41 | public DoubleBufferedLong2ObjectHashMap(final int capacity, final float loadFactor) { 42 | this.mapLocal = new Long2ObjectOpenHashMap<>(capacity, loadFactor); 43 | this.mapShared = new Long2ObjectOpenHashMap<>(capacity, loadFactor); 44 | this.mapPending = new Long2ObjectOpenHashMap<>(capacity, loadFactor); 45 | } 46 | 47 | public V getSync(long k) { 48 | return this.mapLocal.get(k); 49 | } 50 | 51 | public V putSync(long k, V value) { 52 | if (value == null) { 53 | throw new IllegalArgumentException("Value must not be null, use enqueueRemoveSync instead to remove entries"); 54 | } 55 | 56 | this.mapPending.put(k, value); 57 | 58 | return this.mapLocal.put(k, value); 59 | } 60 | 61 | public V removeSync(long k) { 62 | this.mapPending.put(k, null); 63 | 64 | return this.mapLocal.remove(k); 65 | } 66 | 67 | public boolean containsSync(long k) { 68 | return this.mapLocal.containsKey(k); 69 | } 70 | 71 | public V getAsync(long k) { 72 | long stamp; 73 | V ret = null; 74 | 75 | do { 76 | stamp = this.lock.tryOptimisticRead(); 77 | 78 | try { 79 | ret = this.mapShared.get(k); 80 | } catch (ArrayIndexOutOfBoundsException ignored) { } // Swallow memory errors on failed optimistic reads 81 | } while (!this.lock.validate(stamp)); 82 | 83 | return ret; 84 | } 85 | 86 | public void flushChangesSync() { 87 | // Early-exit if there's no work to do 88 | if (this.mapPending.isEmpty()) { 89 | return; 90 | } 91 | 92 | // Swap the local and shared tables immediately, and then block the writer thread while we finish copying 93 | this.swapTables(); 94 | 95 | // Use a non-allocating iterator if possible, otherwise we're going to hurt 96 | for (Long2ObjectMap.Entry entry : Long2ObjectMaps.fastIterable(this.mapPending)) { 97 | final long key = entry.getLongKey(); 98 | final V val = entry.getValue(); 99 | 100 | if (val == null) { 101 | this.mapLocal.remove(key); 102 | } else { 103 | this.mapLocal.put(key, val); 104 | } 105 | } 106 | 107 | this.mapPending.clear(); 108 | } 109 | 110 | private void swapTables() { 111 | final long writeLock = this.lock.writeLock(); 112 | 113 | Long2ObjectMap mapShared = this.mapLocal; 114 | Long2ObjectMap mapLocal = this.mapShared; 115 | 116 | this.mapShared = mapShared; 117 | this.mapLocal = mapLocal; 118 | 119 | this.lock.unlockWrite(writeLock); 120 | } 121 | } -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/math/ChunkSectionPosHelper.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util.math; 2 | 3 | import net.minecraft.util.math.ChunkSectionPos; 4 | 5 | public class ChunkSectionPosHelper { 6 | /** 7 | * Quicker than re-encoding an integer {@link ChunkSectionPos} when you only need to update one coordinate. 8 | * 9 | * @param pos The integer position containing the old X/Z coordinate values 10 | * @param y The new y-coordinate to update {@param pos} with 11 | * @return A new integer ChunkSectionPos which is identical to ChunkSectionPos.asLong(pos.x, y, pos.z) 12 | */ 13 | public static long updateYLong(long pos, int y) { 14 | // [VanillaCopy] ChunkSectionPos static fields 15 | return (pos & ~0xFFFFF) | ((long) y & 0xFFFFF); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/common/util/math/DirectionHelper.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.common.util.math; 2 | 3 | import net.minecraft.util.math.Direction; 4 | 5 | public class DirectionHelper { 6 | /** 7 | * Benchmarks show this to be roughly ~50% faster than {@link Direction#fromVector(int, int, int)}. Input vector 8 | * is not required to be normalized, which also shaves off some CPU overhead. 9 | * 10 | * @param x The x component of the vector 11 | * @param y The y component of the vector 12 | * @param z The z component of the vector 13 | * @return The direction in block space which the vector represents 14 | * @throws IllegalArgumentException If the vector doesn't represent a valid direction 15 | */ 16 | public static Direction getVecDirection(int x, int y, int z) { 17 | if (x == 0 && y < 0 && z == 0) { 18 | return Direction.DOWN; 19 | } else if (x == 0 && y > 0 && z == 0) { 20 | return Direction.UP; 21 | } else if (x == 0 && y == 0 && z < 0) { 22 | return Direction.NORTH; 23 | } else if (x == 0 && y == 0 && z > 0) { 24 | return Direction.SOUTH; 25 | } else if (x < 0 && y == 0 && z == 0) { 26 | return Direction.WEST; 27 | } else if (x > 0 && y == 0 && z == 0) { 28 | return Direction.EAST; 29 | } else { 30 | return null; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/block/MixinAbstractBlockState.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.block; 2 | 3 | import net.caffeinemc.phosphor.common.block.BlockStateLightInfoAccess; 4 | import net.caffeinemc.phosphor.common.block.BlockStateLightInfo; 5 | import net.minecraft.block.AbstractBlock; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | 9 | @Mixin(AbstractBlock.AbstractBlockState.class) 10 | public abstract class MixinAbstractBlockState implements BlockStateLightInfoAccess { 11 | @Shadow 12 | protected AbstractBlock.AbstractBlockState.ShapeCache shapeCache; 13 | 14 | @SuppressWarnings("ConstantConditions") 15 | @Override 16 | public BlockStateLightInfo getLightInfo() { 17 | return (BlockStateLightInfo) (Object) this.shapeCache; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/block/MixinShapeCache.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.block; 2 | 3 | import net.caffeinemc.phosphor.common.block.BlockStateLightInfo; 4 | import net.minecraft.block.AbstractBlock; 5 | import net.minecraft.util.shape.VoxelShape; 6 | import org.spongepowered.asm.mixin.Final; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Shadow; 9 | 10 | @Mixin(AbstractBlock.AbstractBlockState.ShapeCache.class) 11 | public class MixinShapeCache implements BlockStateLightInfo { 12 | @Shadow 13 | @Final 14 | VoxelShape[] extrudedFaces; 15 | 16 | @Shadow 17 | @Final 18 | int lightSubtracted; 19 | 20 | @Override 21 | public VoxelShape[] getExtrudedFaces() { 22 | return this.extrudedFaces; 23 | } 24 | 25 | @Override 26 | public int getLightSubtracted() { 27 | return this.lightSubtracted; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/MixinChunkNibbleArray.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk; 2 | 3 | import net.caffeinemc.phosphor.common.chunk.light.IReadonly; 4 | import net.minecraft.world.chunk.ChunkNibbleArray; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.Overwrite; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | 9 | /** 10 | * An optimized implementation of ChunkNibbleArray which uses bit-banging instead of a conditional to select 11 | * the right bit index of a nibble. 12 | */ 13 | @Mixin(ChunkNibbleArray.class) 14 | public abstract class MixinChunkNibbleArray implements IReadonly { 15 | @Shadow 16 | protected byte[] bytes; 17 | 18 | /** 19 | * @reason Avoid an additional branch. 20 | * @author JellySquid 21 | */ 22 | @Overwrite 23 | public int get(int idx) { 24 | byte[] arr = this.bytes; 25 | 26 | if (arr == null) { 27 | return 0; 28 | } 29 | 30 | int byteIdx = idx >> 1; 31 | int shift = (idx & 1) << 2; 32 | 33 | return (arr[byteIdx] >>> shift) & 15; 34 | } 35 | 36 | /** 37 | * @reason Avoid an additional branch. 38 | * @author JellySquid 39 | */ 40 | @Overwrite 41 | private void set(int idx, int value) { 42 | if (this.isReadonly()) { 43 | throw new UnsupportedOperationException("Cannot modify readonly ChunkNibbleArray"); 44 | } 45 | 46 | byte[] arr = this.bytes; 47 | 48 | if (arr == null) { 49 | this.bytes = (arr = new byte[2048]); 50 | } 51 | 52 | int byteIdx = idx >> 1; 53 | int shift = (idx & 1) << 2; 54 | 55 | arr[byteIdx] = (byte) ((arr[byteIdx] & ~(15 << shift)) 56 | | ((value & 15) << shift)); 57 | } 58 | 59 | @Override 60 | public boolean isReadonly() { 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/MixinChunkStatus.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import net.caffeinemc.phosphor.common.chunk.light.ServerLightingProviderAccess; 5 | import net.minecraft.server.world.ChunkHolder; 6 | import net.minecraft.server.world.ServerLightingProvider; 7 | import net.minecraft.world.Heightmap; 8 | import net.minecraft.world.chunk.Chunk; 9 | import net.minecraft.world.chunk.ChunkStatus; 10 | import org.spongepowered.asm.mixin.Final; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | import org.spongepowered.asm.mixin.Unique; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Redirect; 16 | import org.spongepowered.asm.mixin.injection.Slice; 17 | 18 | import java.util.EnumSet; 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | @Mixin(ChunkStatus.class) 22 | public class MixinChunkStatus { 23 | @Shadow 24 | private static ChunkStatus register(String id, ChunkStatus previous, int taskMargin, EnumSet heightMapTypes, ChunkStatus.ChunkType chunkType, ChunkStatus.GenerationTask task, ChunkStatus.LoadTask noGenTask) { 25 | return null; 26 | } 27 | 28 | @Shadow 29 | @Final 30 | private static ChunkStatus.LoadTask STATUS_BUMP_LOAD_TASK; 31 | 32 | @Redirect( 33 | method = "", 34 | slice = @Slice( 35 | from = @At(value = "CONSTANT", args = "stringValue=features") 36 | ), 37 | at = @At( 38 | value = "INVOKE", 39 | target = "Lnet/minecraft/world/chunk/ChunkStatus;register(Ljava/lang/String;Lnet/minecraft/world/chunk/ChunkStatus;ILjava/util/EnumSet;Lnet/minecraft/world/chunk/ChunkStatus$ChunkType;Lnet/minecraft/world/chunk/ChunkStatus$GenerationTask;Lnet/minecraft/world/chunk/ChunkStatus$LoadTask;)Lnet/minecraft/world/chunk/ChunkStatus;", 40 | ordinal = 0 41 | ) 42 | ) 43 | private static ChunkStatus injectLightmapSetup(final String id, final ChunkStatus previous, final int taskMargin, final EnumSet heightMapTypes, final ChunkStatus.ChunkType chunkType, final ChunkStatus.GenerationTask task, final ChunkStatus.LoadTask loadTask) { 44 | return register(id, previous, taskMargin, heightMapTypes, chunkType, 45 | (status, executor, world, generator, structureManager, lightingProvider, function, surroundingChunks, chunk, force) -> 46 | task.doWork(status, executor, world, generator, structureManager, lightingProvider, function, surroundingChunks, chunk, force).thenCompose( 47 | either -> getPreLightFuture(lightingProvider, either) 48 | ), 49 | (status, world, structureManager, lightingProvider, function, chunk) -> 50 | loadTask.doWork(status, world, structureManager, lightingProvider, function, chunk).thenCompose( 51 | either -> getPreLightFuture(lightingProvider, either) 52 | ) 53 | ); 54 | } 55 | 56 | @Unique 57 | private static CompletableFuture> getPreLightFuture(final ServerLightingProvider lightingProvider, final Either either) { 58 | return either.map( 59 | chunk -> getPreLightFuture(lightingProvider, chunk), 60 | unloaded -> CompletableFuture.completedFuture(Either.right(unloaded)) 61 | ); 62 | } 63 | 64 | @Unique 65 | private static CompletableFuture> getPreLightFuture(final ServerLightingProvider lightingProvider, final Chunk chunk) { 66 | return ((ServerLightingProviderAccess) lightingProvider).setupLightmaps(chunk).thenApply(Either::left); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/MixinProtoChunk.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk; 2 | 3 | import net.minecraft.block.BlockState; 4 | import net.minecraft.util.math.BlockPos; 5 | import net.minecraft.world.chunk.ChunkSection; 6 | import net.minecraft.world.chunk.ChunkStatus; 7 | import net.minecraft.world.chunk.ProtoChunk; 8 | import net.minecraft.world.chunk.light.LightingProvider; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.Unique; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.Slice; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 16 | import org.spongepowered.asm.mixin.injection.callback.LocalCapture; 17 | 18 | @Mixin(ProtoChunk.class) 19 | public abstract class MixinProtoChunk { 20 | @Shadow 21 | private volatile LightingProvider lightingProvider; 22 | 23 | @Shadow 24 | public abstract ChunkStatus getStatus(); 25 | 26 | @Unique 27 | private static final ChunkStatus PRE_LIGHT = ChunkStatus.LIGHT.getPrevious(); 28 | 29 | @Inject( 30 | method = "setBlockState(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Z)Lnet/minecraft/block/BlockState;", 31 | at = @At( 32 | value = "INVOKE", 33 | target = "Lnet/minecraft/world/chunk/ChunkSection;setBlockState(IIILnet/minecraft/block/BlockState;)Lnet/minecraft/block/BlockState;" 34 | ), 35 | locals = LocalCapture.CAPTURE_FAILHARD 36 | ) 37 | private void addLightmap(final BlockPos pos, final BlockState state, final boolean moved, final CallbackInfoReturnable ci, final int x, final int y, final int z, final int index, final ChunkSection section) { 38 | if (this.getStatus().isAtLeast(PRE_LIGHT) && section.isEmpty()) { 39 | this.lightingProvider.setSectionStatus(pos, false); 40 | } 41 | } 42 | 43 | @Inject( 44 | method = "setBlockState(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Z)Lnet/minecraft/block/BlockState;", 45 | slice = @Slice( 46 | from = @At( 47 | value = "INVOKE", 48 | target = "Lnet/minecraft/world/chunk/ChunkSection;setBlockState(IIILnet/minecraft/block/BlockState;)Lnet/minecraft/block/BlockState;" 49 | ) 50 | ), 51 | at = @At( 52 | value = "RETURN", 53 | ordinal = 0 54 | ), 55 | locals = LocalCapture.CAPTURE_FAILHARD 56 | ) 57 | private void removeLightmap(final BlockPos pos, final BlockState state, final boolean moved, final CallbackInfoReturnable ci, final int x, final int y, final int z, final int index, final ChunkSection section) { 58 | if (this.getStatus().isAtLeast(PRE_LIGHT) && section.isEmpty()) { 59 | this.lightingProvider.setSectionStatus(pos, true); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/MixinWorldChunk.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk; 2 | 3 | import net.minecraft.block.BlockState; 4 | import net.minecraft.util.math.BlockPos; 5 | import net.minecraft.util.math.ChunkPos; 6 | import net.minecraft.util.registry.Registry; 7 | import net.minecraft.world.HeightLimitView; 8 | import net.minecraft.world.biome.Biome; 9 | import net.minecraft.world.chunk.Chunk; 10 | import net.minecraft.world.chunk.ChunkSection; 11 | import net.minecraft.world.chunk.UpgradeData; 12 | import net.minecraft.world.chunk.WorldChunk; 13 | import net.minecraft.world.gen.chunk.BlendingData; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.Overwrite; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.stream.Stream; 20 | 21 | @Mixin(WorldChunk.class) 22 | public abstract class MixinWorldChunk extends Chunk { 23 | private MixinWorldChunk(ChunkPos pos, UpgradeData upgradeData, HeightLimitView heightLimitView, Registry biome, long inhabitedTime, ChunkSection[] sectionArrayInitializer, BlendingData blendingData) { 24 | super(pos, upgradeData, heightLimitView, biome, inhabitedTime, sectionArrayInitializer, blendingData); 25 | } 26 | 27 | /** 28 | * This implementation avoids iterating over empty chunk sections and uses direct access to read out block states 29 | * instead. Instead of allocating a BlockPos for every block in the chunk, they're now only allocated once we find 30 | * a light source. 31 | * 32 | * @reason Use optimized implementation 33 | * @author JellySquid 34 | */ 35 | @Overwrite 36 | public Stream getLightSourcesStream() { 37 | List list = new ArrayList<>(); 38 | 39 | int startX = this.pos.getStartX(); 40 | int startZ = this.pos.getStartZ(); 41 | 42 | ChunkSection[] chunkSections = this.sectionArray; 43 | 44 | for (ChunkSection section : chunkSections) { 45 | if (section == null || section.isEmpty()) { 46 | continue; 47 | } 48 | 49 | int startY = section.getYOffset(); 50 | 51 | for (int x = 0; x < 16; x++) { 52 | for (int y = 0; y < 16; y++) { 53 | for (int z = 0; z < 16; z++) { 54 | BlockState state = section.getBlockState(x, y, z); 55 | 56 | if (state.getLuminance() != 0) { 57 | list.add(new BlockPos(startX + x, startY + y, startZ + z)); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | if (list.isEmpty()) { 65 | return Stream.empty(); 66 | } 67 | 68 | return list.stream(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinBlockLightStorage.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import it.unimi.dsi.fastutil.ints.IntIterable; 4 | import it.unimi.dsi.fastutil.longs.LongOpenHashSet; 5 | import it.unimi.dsi.fastutil.longs.LongSet; 6 | import net.caffeinemc.phosphor.common.chunk.light.BlockLightStorageAccess; 7 | import net.minecraft.util.math.ChunkSectionPos; 8 | import net.minecraft.world.chunk.ChunkNibbleArray; 9 | import net.minecraft.world.chunk.light.BlockLightStorage; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Unique; 12 | 13 | @Mixin(BlockLightStorage.class) 14 | public abstract class MixinBlockLightStorage extends MixinLightStorage implements BlockLightStorageAccess { 15 | @Unique 16 | private final LongSet lightEnabled = new LongOpenHashSet(); 17 | 18 | @Override 19 | protected void setColumnEnabled(final long chunkPos, final boolean enable) { 20 | if (enable) { 21 | this.lightEnabled.add(chunkPos); 22 | } else { 23 | this.lightEnabled.remove(chunkPos); 24 | } 25 | } 26 | 27 | @Override 28 | public boolean isLightEnabled(final long sectionPos) { 29 | return this.lightEnabled.contains(ChunkSectionPos.withZeroY(sectionPos)); 30 | } 31 | 32 | @Override 33 | protected int getLightmapComplexityChange(final long blockPos, final int oldVal, final int newVal, final ChunkNibbleArray lightmap) { 34 | return newVal - oldVal; 35 | } 36 | 37 | @Override 38 | protected int getInitialLightmapComplexity(final long sectionPos, final ChunkNibbleArray lightmap) { 39 | if (lightmap.isUninitialized()) { 40 | return 0; 41 | } 42 | 43 | int complexity = 0; 44 | 45 | for (int y = 0; y < 16; ++y) { 46 | for (int z = 0; z < 16; ++z) { 47 | for (int x = 0; x < 16; ++x) { 48 | complexity += lightmap.get(x, y, z); 49 | } 50 | } 51 | } 52 | 53 | return complexity; 54 | } 55 | 56 | @Override 57 | protected void beforeChunkEnabled(final long chunkPos) { 58 | // onLoadSection() does nothing 59 | } 60 | 61 | @Override 62 | protected void afterChunkDisabled(final long chunkPos, final IntIterable removedLightmaps) { 63 | // onUnloadSection() does nothing 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinBlockLightStorageData.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.chunk.light.SharedBlockLightData; 4 | import net.caffeinemc.phosphor.common.util.collections.DoubleBufferedLong2ObjectHashMap; 5 | import net.minecraft.world.chunk.ChunkNibbleArray; 6 | import net.minecraft.world.chunk.light.BlockLightStorage; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Overwrite; 9 | 10 | @Mixin(BlockLightStorage.Data.class) 11 | public abstract class MixinBlockLightStorageData extends MixinChunkToNibbleArrayMap implements SharedBlockLightData { 12 | @Override 13 | public void makeSharedCopy(final DoubleBufferedLong2ObjectHashMap queue) { 14 | super.makeSharedCopy(queue); 15 | } 16 | 17 | /** 18 | * @reason Use double-buffering to avoid copying 19 | * @author JellySquid 20 | */ 21 | @SuppressWarnings("ConstantConditions") 22 | @Overwrite 23 | public BlockLightStorage.Data copy() { 24 | // This will be called immediately by LightStorage in the constructor 25 | // We can take advantage of this fact to initialize our extra properties here without additional hacks 26 | if (!this.isInitialized()) { 27 | this.init(); 28 | } 29 | 30 | BlockLightStorage.Data data = new BlockLightStorage.Data(this.arrays); 31 | ((SharedBlockLightData) (Object) data).makeSharedCopy(this.getUpdateQueue()); 32 | 33 | return data; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinChunkBlockLightProvider.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.chunk.light.BlockLightStorageAccess; 4 | import net.caffeinemc.phosphor.common.util.LightUtil; 5 | import net.caffeinemc.phosphor.common.util.math.DirectionHelper; 6 | import net.minecraft.block.BlockState; 7 | import net.minecraft.util.math.BlockPos; 8 | import net.minecraft.util.math.ChunkSectionPos; 9 | import net.minecraft.util.math.Direction; 10 | import net.minecraft.util.shape.VoxelShape; 11 | import net.minecraft.world.chunk.light.ChunkBlockLightProvider; 12 | import org.spongepowered.asm.mixin.Final; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Overwrite; 15 | import org.spongepowered.asm.mixin.Shadow; 16 | 17 | import static net.minecraft.util.math.ChunkSectionPos.getSectionCoord; 18 | 19 | @Mixin(ChunkBlockLightProvider.class) 20 | public abstract class MixinChunkBlockLightProvider extends MixinChunkLightProvider { 21 | @Shadow 22 | protected abstract int getLightSourceLuminance(long blockPos); 23 | 24 | @Shadow 25 | @Final 26 | private static Direction[] DIRECTIONS; 27 | 28 | /** 29 | * @reason Use optimized variant 30 | * @author JellySquid 31 | */ 32 | @Override 33 | @Overwrite 34 | public int getPropagatedLevel(long fromId, long toId, int currentLevel) { 35 | return this.getPropagatedLevel(fromId, null, toId, currentLevel); 36 | } 37 | 38 | /** 39 | * This breaks up the call to method_20479 into smaller parts so we do not have to pass a mutable heap object 40 | * to the method in order to extract the light result. This has a few other advantages, allowing us to: 41 | * - Avoid the de-optimization that occurs from allocating and passing a heap object 42 | * - Avoid unpacking coordinates twice for both the call to method_20479 and method_20710. 43 | * - Avoid the the specific usage of AtomicInteger, which has additional overhead for the atomic get/set operations. 44 | * - Avoid checking if the checked block is opaque twice. 45 | * - Avoid a redundant block state lookup by re-using {@param fromState} 46 | *

47 | * The rest of the implementation has been otherwise copied from vanilla, but is optimized to avoid constantly 48 | * (un)packing coordinates and to use an optimized direction lookup function. 49 | * 50 | * @param fromState The re-usable block state at position {@param fromId} 51 | * @author JellySquid 52 | */ 53 | @Override 54 | public int getPropagatedLevel(long fromId, BlockState fromState, long toId, int currentLevel) { 55 | if (toId == Long.MAX_VALUE) { 56 | return 15; 57 | } else if (fromId == Long.MAX_VALUE && ((BlockLightStorageAccess) this.lightStorage).isLightEnabled(ChunkSectionPos.fromBlockPos(toId))) { 58 | // Disable blocklight sources before initial lighting 59 | return currentLevel + 15 - this.getLightSourceLuminance(toId); 60 | } else if (currentLevel >= 15) { 61 | return currentLevel; 62 | } 63 | 64 | int toX = BlockPos.unpackLongX(toId); 65 | int toY = BlockPos.unpackLongY(toId); 66 | int toZ = BlockPos.unpackLongZ(toId); 67 | 68 | int fromX = BlockPos.unpackLongX(fromId); 69 | int fromY = BlockPos.unpackLongY(fromId); 70 | int fromZ = BlockPos.unpackLongZ(fromId); 71 | 72 | Direction dir = DirectionHelper.getVecDirection(toX - fromX, toY - fromY, toZ - fromZ); 73 | 74 | if (dir != null) { 75 | BlockState toState = this.getBlockStateForLighting(toX, toY, toZ); 76 | 77 | if (toState == null) { 78 | return 15; 79 | } 80 | 81 | int newLevel = this.getSubtractedLight(toState, toX, toY, toZ); 82 | 83 | if (newLevel >= 15) { 84 | return 15; 85 | } 86 | 87 | if (fromState == null) { 88 | fromState = this.getBlockStateForLighting(fromX, fromY, fromZ); 89 | } 90 | 91 | VoxelShape aShape = this.getOpaqueShape(fromState, fromX, fromY, fromZ, dir); 92 | VoxelShape bShape = this.getOpaqueShape(toState, toX, toY, toZ, dir.getOpposite()); 93 | 94 | if (!LightUtil.unionCoversFullCube(aShape, bShape)) { 95 | return currentLevel + Math.max(1, newLevel); 96 | } 97 | } 98 | 99 | return 15; 100 | } 101 | 102 | /** 103 | * Avoids constantly (un)packing coordinates. This strictly copies vanilla's implementation. 104 | * @reason Use faster implementation 105 | * @author JellySquid 106 | */ 107 | @Overwrite 108 | public void propagateLevel(long id, int targetLevel, boolean mergeAsMin) { 109 | int x = BlockPos.unpackLongX(id); 110 | int y = BlockPos.unpackLongY(id); 111 | int z = BlockPos.unpackLongZ(id); 112 | 113 | long chunk = ChunkSectionPos.asLong(getSectionCoord(x), getSectionCoord(y), getSectionCoord(z)); 114 | 115 | BlockState state = this.getBlockStateForLighting(x, y, z); 116 | 117 | for (Direction dir : DIRECTIONS) { 118 | int adjX = x + dir.getOffsetX(); 119 | int adjY = y + dir.getOffsetY(); 120 | int adjZ = z + dir.getOffsetZ(); 121 | 122 | long adjChunk = ChunkSectionPos.asLong(getSectionCoord(adjX), getSectionCoord(adjY), getSectionCoord(adjZ)); 123 | 124 | if ((chunk == adjChunk) || this.hasSection(adjChunk)) { 125 | this.propagateLevel(id, state, BlockPos.asLong(adjX, adjY, adjZ), targetLevel, mergeAsMin); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinChunkLightProvider.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; 4 | import net.caffeinemc.phosphor.common.block.BlockStateLightInfo; 5 | import net.caffeinemc.phosphor.common.block.BlockStateLightInfoAccess; 6 | import net.caffeinemc.phosphor.common.chunk.light.InitialLightingAccess; 7 | import net.caffeinemc.phosphor.common.chunk.light.LightProviderUpdateTracker; 8 | import net.caffeinemc.phosphor.common.chunk.light.LightStorageAccess; 9 | import net.minecraft.block.BlockState; 10 | import net.minecraft.block.Blocks; 11 | import net.minecraft.util.math.BlockPos; 12 | import net.minecraft.util.math.ChunkPos; 13 | import net.minecraft.util.math.ChunkSectionPos; 14 | import net.minecraft.util.math.Direction; 15 | import net.minecraft.util.shape.VoxelShape; 16 | import net.minecraft.util.shape.VoxelShapes; 17 | import net.minecraft.world.chunk.Chunk; 18 | import net.minecraft.world.chunk.ChunkProvider; 19 | import net.minecraft.world.chunk.ChunkSection; 20 | import net.minecraft.world.chunk.light.ChunkLightProvider; 21 | import net.minecraft.world.chunk.light.LightStorage; 22 | import org.spongepowered.asm.mixin.Final; 23 | import org.spongepowered.asm.mixin.Mixin; 24 | import org.spongepowered.asm.mixin.Overwrite; 25 | import org.spongepowered.asm.mixin.Shadow; 26 | import org.spongepowered.asm.mixin.Unique; 27 | import org.spongepowered.asm.mixin.injection.At; 28 | import org.spongepowered.asm.mixin.injection.Inject; 29 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 30 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 31 | 32 | import java.util.Arrays; 33 | import java.util.BitSet; 34 | 35 | @Mixin(ChunkLightProvider.class) 36 | public abstract class MixinChunkLightProvider 37 | extends MixinLevelPropagator implements InitialLightingAccess, LightProviderUpdateTracker { 38 | private static final BlockState DEFAULT_STATE = Blocks.AIR.getDefaultState(); 39 | private static final ChunkSection[] EMPTY_SECTION_ARRAY = new ChunkSection[0]; 40 | 41 | @Shadow 42 | @Final 43 | protected BlockPos.Mutable reusableBlockPos; 44 | 45 | @Shadow 46 | @Final 47 | protected ChunkProvider chunkProvider; 48 | 49 | private final long[] cachedChunkPos = new long[2]; 50 | private final ChunkSection[][] cachedChunkSections = new ChunkSection[2][]; 51 | 52 | private final Long2ObjectOpenHashMap buckets = new Long2ObjectOpenHashMap<>(); 53 | 54 | private long prevChunkBucketKey = ChunkPos.MARKER; 55 | private BitSet prevChunkBucketSet; 56 | 57 | @Inject(method = "clearChunkCache", at = @At("RETURN")) 58 | @SuppressWarnings("ConstantConditions") 59 | private void onCleanup(CallbackInfo ci) { 60 | // This callback may be executed from the constructor above, and the object won't be initialized then 61 | if (this.cachedChunkPos != null) { 62 | Arrays.fill(this.cachedChunkPos, ChunkPos.MARKER); 63 | Arrays.fill(this.cachedChunkSections, null); 64 | } 65 | } 66 | 67 | @Unique 68 | protected boolean hasSection(final long sectionPos) { 69 | return ((LightStorageAccess) this.lightStorage).callHasSection(sectionPos); 70 | } 71 | 72 | // [VanillaCopy] method_20479 73 | /** 74 | * Returns the BlockState which represents the block at the specified coordinates in the world. This may return 75 | * a different BlockState than what actually exists at the coordinates (such as if it is out of bounds), but will 76 | * always represent a state with valid light properties for that coordinate. 77 | */ 78 | @Unique 79 | protected BlockState getBlockStateForLighting(int x, int y, int z) { 80 | final long chunkPos = ChunkPos.toLong(x >> 4, z >> 4); 81 | 82 | for (int i = 0; i < 2; i++) { 83 | if (this.cachedChunkPos[i] == chunkPos) { 84 | return this.getBlockStateFromSection(this.cachedChunkSections[i], x, y, z); 85 | } 86 | } 87 | 88 | return this.getBlockStateForLightingUncached(x, y, z); 89 | } 90 | 91 | private BlockState getBlockStateForLightingUncached(int x, int y, int z) { 92 | return this.getBlockStateFromSection(this.getAndCacheChunkSections(x >> 4, z >> 4), x, y, z); 93 | } 94 | 95 | private BlockState getBlockStateFromSection(final ChunkSection[] sections, final int x, final int y, final int z) { 96 | final int index = this.chunkProvider.getWorld().getSectionIndex(y); 97 | 98 | if (index >= 0 && index < sections.length) { 99 | final ChunkSection section = sections[index]; 100 | 101 | if (!section.isEmpty()) { 102 | return section.getBlockState(x & 15, y & 15, z & 15); 103 | } 104 | } 105 | 106 | return DEFAULT_STATE; 107 | } 108 | 109 | private ChunkSection[] getAndCacheChunkSections(int x, int z) { 110 | final Chunk chunk = (Chunk) this.chunkProvider.getChunk(x, z); 111 | final ChunkSection[] sections = chunk != null ? chunk.getSectionArray() : EMPTY_SECTION_ARRAY; 112 | 113 | final ChunkSection[][] cachedSections = this.cachedChunkSections; 114 | cachedSections[1] = cachedSections[0]; 115 | cachedSections[0] = sections; 116 | 117 | final long[] cachedCoords = this.cachedChunkPos; 118 | cachedCoords[1] = cachedCoords[0]; 119 | cachedCoords[0] = ChunkPos.toLong(x, z); 120 | 121 | return sections; 122 | } 123 | 124 | // [VanillaCopy] method_20479 125 | /** 126 | * Returns the amount of light which is blocked at the specified coordinates by the BlockState. 127 | */ 128 | @Unique 129 | protected int getSubtractedLight(BlockState state, int x, int y, int z) { 130 | BlockStateLightInfo info = ((BlockStateLightInfoAccess) state).getLightInfo(); 131 | 132 | if (info != null) { 133 | return info.getLightSubtracted(); 134 | } else { 135 | return this.getSubtractedLightFallback(state, x, y, z); 136 | } 137 | } 138 | 139 | private int getSubtractedLightFallback(BlockState state, int x, int y, int z) { 140 | return state.getBlock().getOpacity(state, this.chunkProvider.getWorld(), this.reusableBlockPos.set(x, y, z)); 141 | } 142 | 143 | // [VanillaCopy] method_20479 144 | /** 145 | * Returns the VoxelShape of a block for lighting without making a second call to 146 | * {@link #getBlockStateForLighting(int, int, int)}. 147 | */ 148 | @Unique 149 | protected VoxelShape getOpaqueShape(BlockState state, int x, int y, int z, Direction dir) { 150 | if (state != null && state.hasSidedTransparency()) { 151 | BlockStateLightInfo info = ((BlockStateLightInfoAccess) state).getLightInfo(); 152 | 153 | if (info != null) { 154 | VoxelShape[] extrudedFaces = info.getExtrudedFaces(); 155 | 156 | if (extrudedFaces != null) { 157 | return extrudedFaces[dir.ordinal()]; 158 | } 159 | } else { 160 | return this.getOpaqueShapeFallback(state, x, y, z, dir); 161 | } 162 | } 163 | 164 | return VoxelShapes.empty(); 165 | } 166 | 167 | private VoxelShape getOpaqueShapeFallback(BlockState state, int x, int y, int z, Direction dir) { 168 | return VoxelShapes.extrudeFace(state.getCullingShape(this.chunkProvider.getWorld(), this.reusableBlockPos.set(x, y, z)), dir); 169 | } 170 | 171 | /** 172 | * The vanilla implementation for removing pending light updates requires iterating over either every queued light 173 | * update (<8K checks) or every block position within a sub-chunk (16^3 checks). This is painfully slow and results 174 | * in a tremendous amount of CPU time being spent here when chunks are unloaded on the client and server. 175 | * 176 | * To work around this, we maintain a bit-field of queued updates by chunk position so we can simply select every 177 | * light update within a section without excessive iteration. The bit-field only requires 64 bytes of memory per 178 | * section with queued updates, and does not require expensive hashing in order to track updates within it. In order 179 | * to avoid as much overhead as possible when looking up a bit-field for a given chunk section, the previous lookup 180 | * is cached and used where possible. The integer key for each bucket can be computed by performing a simple bit 181 | * mask over the already-encoded block position value. 182 | */ 183 | @Override 184 | public void cancelUpdatesForChunk(long sectionPos) { 185 | long key = getBucketKeyForSection(sectionPos); 186 | BitSet bits = this.removeChunkBucket(key); 187 | 188 | if (bits != null && !bits.isEmpty()) { 189 | int startX = ChunkSectionPos.unpackX(sectionPos) << 4; 190 | int startY = ChunkSectionPos.unpackY(sectionPos) << 4; 191 | int startZ = ChunkSectionPos.unpackZ(sectionPos) << 4; 192 | 193 | for (int i = bits.nextSetBit(0); i != -1; i = bits.nextSetBit(i + 1)) { 194 | int x = (i >> 8) & 15; 195 | int y = (i >> 4) & 15; 196 | int z = i & 15; 197 | 198 | this.removePendingUpdate(BlockPos.asLong(startX + x, startY + y, startZ + z)); 199 | } 200 | } 201 | } 202 | 203 | @Override 204 | protected void onPendingUpdateRemoved(long blockPos) { 205 | long key = getBucketKeyForBlock(blockPos); 206 | 207 | BitSet bits; 208 | 209 | if (this.prevChunkBucketKey == key) { 210 | bits = this.prevChunkBucketSet; 211 | } else { 212 | bits = this.buckets.get(key); 213 | 214 | if (bits == null) { 215 | return; 216 | } 217 | } 218 | 219 | bits.clear(getLocalIndex(blockPos)); 220 | 221 | if (bits.isEmpty()) { 222 | this.removeChunkBucket(key); 223 | } 224 | } 225 | 226 | @Override 227 | protected void onPendingUpdateAdded(long blockPos) { 228 | long key = getBucketKeyForBlock(blockPos); 229 | 230 | BitSet bits; 231 | 232 | if (this.prevChunkBucketKey == key) { 233 | bits = this.prevChunkBucketSet; 234 | } else { 235 | bits = this.buckets.get(key); 236 | 237 | if (bits == null) { 238 | this.buckets.put(key, bits = new BitSet(16 * 16 * 16)); 239 | } 240 | 241 | this.prevChunkBucketKey = key; 242 | this.prevChunkBucketSet = bits; 243 | } 244 | 245 | bits.set(getLocalIndex(blockPos)); 246 | } 247 | 248 | // Used to mask a long-encoded block position into a bucket key by dropping the first 4 bits of each component 249 | private static final long BLOCK_TO_BUCKET_KEY_MASK = ~BlockPos.asLong(15, 15, 15); 250 | 251 | private long getBucketKeyForBlock(long blockPos) { 252 | return blockPos & BLOCK_TO_BUCKET_KEY_MASK; 253 | } 254 | 255 | private long getBucketKeyForSection(long sectionPos) { 256 | return BlockPos.asLong(ChunkSectionPos.unpackX(sectionPos) << 4, ChunkSectionPos.unpackY(sectionPos) << 4, ChunkSectionPos.unpackZ(sectionPos) << 4); 257 | } 258 | 259 | private BitSet removeChunkBucket(long key) { 260 | BitSet set = this.buckets.remove(key); 261 | 262 | if (this.prevChunkBucketSet == set) { 263 | this.prevChunkBucketKey = ChunkPos.MARKER; 264 | this.prevChunkBucketSet = null; 265 | } 266 | 267 | return set; 268 | } 269 | 270 | // Finds the bit-flag index of a local position within a chunk section 271 | private static int getLocalIndex(long blockPos) { 272 | int x = BlockPos.unpackLongX(blockPos) & 15; 273 | int y = BlockPos.unpackLongY(blockPos) & 15; 274 | int z = BlockPos.unpackLongZ(blockPos) & 15; 275 | 276 | return (x << 8) | (y << 4) | z; 277 | } 278 | 279 | @Shadow 280 | @Final 281 | protected LightStorage lightStorage; 282 | 283 | @Shadow 284 | protected void resetLevel(long id) {} 285 | 286 | /** 287 | * @author PhiPro 288 | * @reason Re-implement completely. Change specification of the method. 289 | * Now controls both source light and light updates. Disabling now additionally removes all light data associated to the chunk. 290 | */ 291 | @SuppressWarnings("ConstantConditions") 292 | @Overwrite 293 | public void setColumnEnabled(final ChunkPos pos, final boolean enabled) { 294 | final long chunkPos = ChunkSectionPos.withZeroY(ChunkSectionPos.asLong(pos.x, 0, pos.z)); 295 | final LightStorageAccess lightStorage = (LightStorageAccess) this.lightStorage; 296 | 297 | if (enabled) { 298 | lightStorage.invokeSetColumnEnabled(chunkPos, true); 299 | lightStorage.enableLightUpdates(chunkPos); 300 | } else { 301 | lightStorage.disableChunkLight(chunkPos, (ChunkLightProvider) (Object) this); 302 | } 303 | } 304 | 305 | @Override 306 | public void enableSourceLight(final long chunkPos) { 307 | ((LightStorageAccess) this.lightStorage).invokeSetColumnEnabled(chunkPos, true); 308 | } 309 | 310 | @Override 311 | public void enableLightUpdates(final long chunkPos) { 312 | ((LightStorageAccess) this.lightStorage).enableLightUpdates(chunkPos); 313 | } 314 | 315 | @Inject( 316 | method = "doLightUpdates(IZZ)I", 317 | at = @At( 318 | value = "INVOKE", 319 | target = "Lnet/minecraft/world/chunk/light/LightStorage;notifyChanges()V" 320 | ) 321 | ) 322 | private void runCleanups(final CallbackInfoReturnable ci) { 323 | ((LightStorageAccess) this.lightStorage).runCleanups(); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinChunkSkyLightProvider.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.chunk.light.SkyLightStorageAccess; 4 | import net.caffeinemc.phosphor.common.util.LightUtil; 5 | import net.caffeinemc.phosphor.common.util.math.ChunkSectionPosHelper; 6 | import net.caffeinemc.phosphor.common.util.math.DirectionHelper; 7 | import net.minecraft.block.BlockState; 8 | import net.minecraft.block.Blocks; 9 | import net.minecraft.util.math.BlockPos; 10 | import net.minecraft.util.math.ChunkSectionPos; 11 | import net.minecraft.util.math.Direction; 12 | import net.minecraft.util.shape.VoxelShape; 13 | import net.minecraft.util.shape.VoxelShapes; 14 | import net.minecraft.world.chunk.light.ChunkSkyLightProvider; 15 | import org.spongepowered.asm.mixin.Final; 16 | import org.spongepowered.asm.mixin.Mixin; 17 | import org.spongepowered.asm.mixin.Overwrite; 18 | import org.spongepowered.asm.mixin.Shadow; 19 | 20 | import static net.minecraft.util.math.ChunkSectionPos.getLocalCoord; 21 | import static net.minecraft.util.math.ChunkSectionPos.getSectionCoord; 22 | 23 | @Mixin(ChunkSkyLightProvider.class) 24 | public abstract class MixinChunkSkyLightProvider extends MixinChunkLightProvider { 25 | private static final BlockState AIR_BLOCK = Blocks.AIR.getDefaultState(); 26 | 27 | @Shadow 28 | @Final 29 | private static Direction[] HORIZONTAL_DIRECTIONS; 30 | 31 | @Shadow 32 | @Final 33 | private static Direction[] DIRECTIONS; 34 | 35 | /** 36 | * @author JellySquid 37 | * @reason Use optimized method below 38 | */ 39 | @Override 40 | @Overwrite 41 | public int getPropagatedLevel(long fromId, long toId, int currentLevel) { 42 | return this.getPropagatedLevel(fromId, null, toId, currentLevel); 43 | } 44 | 45 | /** 46 | * This breaks up the call to method_20479 into smaller parts so we do not have to pass a mutable heap object 47 | * to the method in order to extract the light result. This has a few other advantages, allowing us to: 48 | * - Avoid the de-optimization that occurs from allocating and passing a heap object 49 | * - Avoid unpacking coordinates twice for both the call to method_20479 and method_20710. 50 | * - Avoid the the specific usage of AtomicInteger, which has additional overhead for the atomic get/set operations. 51 | * - Avoid checking if the checked block is opaque twice. 52 | * - Avoid a redundant block state lookup by re-using {@param fromState} 53 | * 54 | * The rest of the implementation has been otherwise copied from vanilla, but is optimized to avoid constantly 55 | * (un)packing coordinates and to use an optimized direction lookup function. 56 | * 57 | * @param fromState The re-usable block state at position {@param fromId} 58 | */ 59 | @Override 60 | public int getPropagatedLevel(long fromId, BlockState fromState, long toId, int currentLevel) { 61 | if (toId == Long.MAX_VALUE || fromId == Long.MAX_VALUE) { 62 | return 15; 63 | } else if (currentLevel >= 15) { 64 | return currentLevel; 65 | } 66 | 67 | int toX = BlockPos.unpackLongX(toId); 68 | int toY = BlockPos.unpackLongY(toId); 69 | int toZ = BlockPos.unpackLongZ(toId); 70 | 71 | BlockState toState = this.getBlockStateForLighting(toX, toY, toZ); 72 | 73 | int fromX = BlockPos.unpackLongX(fromId); 74 | int fromY = BlockPos.unpackLongY(fromId); 75 | int fromZ = BlockPos.unpackLongZ(fromId); 76 | 77 | if (fromState == null) { 78 | fromState = this.getBlockStateForLighting(fromX, fromY, fromZ); 79 | } 80 | 81 | // Most light updates will happen between two empty air blocks, so use this to assume some properties 82 | boolean airPropagation = toState == AIR_BLOCK && fromState == AIR_BLOCK; 83 | boolean verticalOnly = fromX == toX && fromZ == toZ; 84 | 85 | // The direction the light update is propagating 86 | Direction dir = DirectionHelper.getVecDirection(toX - fromX, toY - fromY, toZ - fromZ); 87 | 88 | if (dir == null) { 89 | throw new IllegalStateException(String.format("Light was spread in illegal direction %d, %d, %d", Integer.signum(toX - fromX), Integer.signum(toY - fromY), Integer.signum(toZ - fromZ))); 90 | } 91 | 92 | // Shape comparison checks are only meaningful if the blocks involved have non-empty shapes 93 | // If we're comparing between air blocks, this is meaningless 94 | if (!airPropagation) { 95 | VoxelShape toShape = this.getOpaqueShape(toState, toX, toY, toZ, dir.getOpposite()); 96 | 97 | if (toShape != VoxelShapes.fullCube()) { 98 | VoxelShape fromShape = this.getOpaqueShape(fromState, fromX, fromY, fromZ, dir); 99 | 100 | if (LightUtil.unionCoversFullCube(fromShape, toShape)) { 101 | return 15; 102 | } 103 | } 104 | } 105 | 106 | int out = this.getSubtractedLight(toState, toX, toY, toZ); 107 | 108 | if (out == 0 && currentLevel == 0 && verticalOnly && fromY > toY) { 109 | return 0; 110 | } 111 | 112 | return currentLevel + Math.max(1, out); 113 | } 114 | 115 | /** 116 | * A few key optimizations are made here, in particular: 117 | * - The code avoids un-packing coordinates as much as possible and stores the results into local variables. 118 | * - When necessary, coordinate re-packing is reduced to the minimum number of operations. Most of them can be reduced 119 | * to only updating the Y-coordinate versus re-computing the entire integer. 120 | * - Coordinate re-packing is removed where unnecessary (such as when only comparing the Y-coordinate of two positions) 121 | * - A special propagation method is used that allows the BlockState at {@param id} to be passed, allowing the code 122 | * which follows to simply re-use it instead of redundantly retrieving another block state. 123 | * 124 | * Additionally this implements the cleanups discussed in MC-196542. In particular: 125 | * - This always passes adjacent positions to {@link #propagateLevel(long, BlockState, long, int, boolean)} 126 | * - This reduces map lookups in the skylight optimization code 127 | * 128 | * @reason Use faster implementation 129 | * @author JellySquid, PhiPro 130 | */ 131 | @Overwrite 132 | public void propagateLevel(long id, int targetLevel, boolean mergeAsMin) { 133 | long chunkId = ChunkSectionPos.fromBlockPos(id); 134 | 135 | int x = BlockPos.unpackLongX(id); 136 | int y = BlockPos.unpackLongY(id); 137 | int z = BlockPos.unpackLongZ(id); 138 | 139 | int localX = getLocalCoord(x); 140 | int localY = getLocalCoord(y); 141 | int localZ = getLocalCoord(z); 142 | 143 | BlockState fromState = this.getBlockStateForLighting(x, y, z); 144 | 145 | // Fast-path: Use much simpler logic if we do not need to access adjacent chunks 146 | if (localX > 0 && localX < 15 && localY > 0 && localY < 15 && localZ > 0 && localZ < 15) { 147 | for (Direction dir : DIRECTIONS) { 148 | this.propagateLevel(id, fromState, BlockPos.asLong(x + dir.getOffsetX(), y + dir.getOffsetY(), z + dir.getOffsetZ()), targetLevel, mergeAsMin); 149 | } 150 | 151 | return; 152 | } 153 | 154 | int chunkY = getSectionCoord(y); 155 | int chunkOffsetY = 0; 156 | 157 | // Skylight optimization: Try to find bottom-most non-empty chunk 158 | if (localY == 0) { 159 | while (!this.hasSection(ChunkSectionPos.offset(chunkId, 0, -chunkOffsetY - 1, 0)) 160 | && ((SkyLightStorageAccess) this.lightStorage).callIsAboveMinHeight(chunkY - chunkOffsetY - 1)) { 161 | ++chunkOffsetY; 162 | } 163 | } 164 | 165 | int belowY = y + (-1 - chunkOffsetY * 16); 166 | int belowChunkY = getSectionCoord(belowY); 167 | 168 | if (chunkY == belowChunkY || this.hasSection(ChunkSectionPosHelper.updateYLong(chunkId, belowChunkY))) { 169 | // MC-196542: Pass adjacent source position 170 | BlockState state = chunkY == belowChunkY ? fromState : AIR_BLOCK; 171 | this.propagateLevel(BlockPos.asLong(x, belowY + 1, z), state, BlockPos.asLong(x, belowY, z), targetLevel, mergeAsMin); 172 | } 173 | 174 | int aboveY = y + 1; 175 | int aboveChunkY = getSectionCoord(aboveY); 176 | 177 | if (chunkY == aboveChunkY || this.hasSection(ChunkSectionPosHelper.updateYLong(chunkId, aboveChunkY))) { 178 | this.propagateLevel(id, fromState, BlockPos.asLong(x, aboveY, z), targetLevel, mergeAsMin); 179 | } 180 | 181 | for (Direction dir : HORIZONTAL_DIRECTIONS) { 182 | int adjX = x + dir.getOffsetX(); 183 | int adjZ = z + dir.getOffsetZ(); 184 | 185 | long offsetId = BlockPos.asLong(adjX, y, adjZ); 186 | long offsetChunkId = ChunkSectionPos.fromBlockPos(offsetId); 187 | 188 | boolean isWithinOriginChunk = chunkId == offsetChunkId; 189 | 190 | if (isWithinOriginChunk || this.hasSection(offsetChunkId)) { 191 | this.propagateLevel(id, fromState, offsetId, targetLevel, mergeAsMin); 192 | } 193 | 194 | if (isWithinOriginChunk) { 195 | continue; 196 | } 197 | 198 | // MC-196542: First iterate over sections to reduce map lookups 199 | for (int offsetChunkY = chunkY - 1; offsetChunkY > belowChunkY; --offsetChunkY) { 200 | if (!this.hasSection(ChunkSectionPosHelper.updateYLong(offsetChunkId, offsetChunkY))) { 201 | continue; 202 | } 203 | 204 | for (int offsetY = 15; offsetY >= 0; --offsetY) { 205 | int adjY = ChunkSectionPos.getBlockCoord(offsetChunkY) + offsetY; 206 | offsetId = BlockPos.asLong(adjX, adjY, adjZ); 207 | 208 | this.propagateLevel(BlockPos.asLong(x, adjY, z), AIR_BLOCK, offsetId, targetLevel, mergeAsMin); 209 | } 210 | } 211 | } 212 | } 213 | 214 | /** 215 | * @author PhiPro 216 | * @reason MC-196542: There is no need to reset any level other than the directly requested position. 217 | */ 218 | @Overwrite 219 | public void resetLevel(long id) { 220 | super.resetLevel(id); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinChunkToNibbleArrayMap.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; 4 | import net.caffeinemc.phosphor.common.util.collections.DoubleBufferedLong2ObjectHashMap; 5 | import net.minecraft.world.chunk.ChunkNibbleArray; 6 | import net.minecraft.world.chunk.ChunkToNibbleArrayMap; 7 | import org.spongepowered.asm.mixin.Final; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Overwrite; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.Unique; 12 | 13 | @Mixin(ChunkToNibbleArrayMap.class) 14 | public abstract class MixinChunkToNibbleArrayMap { 15 | @Shadow 16 | private boolean cacheEnabled; 17 | 18 | @Shadow 19 | @Final 20 | private long[] cachePositions; 21 | 22 | @Shadow 23 | @Final 24 | private ChunkNibbleArray[] cacheArrays; 25 | 26 | @Shadow 27 | public abstract void clearCache(); 28 | 29 | @Shadow 30 | @Final 31 | protected Long2ObjectOpenHashMap arrays; 32 | 33 | @Unique 34 | private DoubleBufferedLong2ObjectHashMap queue; 35 | @Unique 36 | private boolean isShared; 37 | // Indicates whether or not the extended data structures have been initialized 38 | @Unique 39 | private boolean init; 40 | 41 | @Unique 42 | protected boolean isShared() { 43 | return this.isShared; 44 | } 45 | 46 | @Unique 47 | protected boolean isInitialized() { 48 | return this.init; 49 | } 50 | 51 | /** 52 | * @reason Allow shared access, avoid copying 53 | * @author JellySquid 54 | */ 55 | @Overwrite 56 | public void replaceWithCopy(long pos) { 57 | this.checkExclusiveOwner(); 58 | 59 | this.queue.putSync(pos, this.queue.getSync(pos).copy()); 60 | 61 | this.clearCache(); 62 | } 63 | 64 | /** 65 | * @reason Allow shared access, avoid copying 66 | * @author JellySquid 67 | */ 68 | @SuppressWarnings("OverwriteModifiers") 69 | @Overwrite 70 | public ChunkNibbleArray get(long pos) { 71 | if (this.cacheEnabled) { 72 | // Hoist array field access out of the loop to allow the JVM to drop bounds checks 73 | long[] cachePositions = this.cachePositions; 74 | 75 | for(int i = 0; i < cachePositions.length; ++i) { 76 | if (pos == cachePositions[i]) { 77 | return this.cacheArrays[i]; 78 | } 79 | } 80 | } 81 | 82 | // Move to a separate method to help the JVM inline methods 83 | return this.getUncached(pos); 84 | } 85 | 86 | @Unique 87 | private ChunkNibbleArray getUncached(long pos) { 88 | ChunkNibbleArray array; 89 | 90 | if (this.isShared) { 91 | array = this.queue.getAsync(pos); 92 | } else { 93 | array = this.queue.getSync(pos); 94 | } 95 | 96 | if (array == null) { 97 | return null; 98 | } 99 | 100 | if (this.cacheEnabled) { 101 | long[] cachePositions = this.cachePositions; 102 | ChunkNibbleArray[] cacheArrays = this.cacheArrays; 103 | 104 | for(int i = cacheArrays.length - 1; i > 0; --i) { 105 | cachePositions[i] = cachePositions[i - 1]; 106 | cacheArrays[i] = cacheArrays[i - 1]; 107 | } 108 | 109 | cachePositions[0] = pos; 110 | cacheArrays[0] = array; 111 | } 112 | 113 | return array; 114 | } 115 | 116 | /** 117 | * @reason Allow shared access, avoid copying 118 | * @author JellySquid 119 | */ 120 | @Overwrite 121 | public void put(long pos, ChunkNibbleArray data) { 122 | this.checkExclusiveOwner(); 123 | 124 | this.queue.putSync(pos, data); 125 | } 126 | 127 | /** 128 | * @reason Allow shared access, avoid copying 129 | * @author JellySquid 130 | */ 131 | @SuppressWarnings("OverwriteModifiers") 132 | @Overwrite 133 | public ChunkNibbleArray removeChunk(long chunkPos) { 134 | this.checkExclusiveOwner(); 135 | 136 | return this.queue.removeSync(chunkPos); 137 | } 138 | 139 | /** 140 | * @reason Allow shared access, avoid copying 141 | * @author JellySquid 142 | */ 143 | @Overwrite 144 | public boolean containsKey(long chunkPos) { 145 | if (this.isShared) { 146 | return this.queue.getAsync(chunkPos) != null; 147 | } else { 148 | return this.queue.containsSync(chunkPos); 149 | } 150 | } 151 | 152 | /** 153 | * Check if the light array table is exclusively owned (not shared). If not, an exception is thrown to catch the 154 | * invalid state. Synchronous writes can only occur while the table is exclusively owned by the writer/actor thread. 155 | */ 156 | @Unique 157 | protected void checkExclusiveOwner() { 158 | if (this.isShared) { 159 | throw new IllegalStateException("Tried to synchronously write to light data array table after it was made shareable"); 160 | } 161 | } 162 | 163 | /** 164 | * Returns the queue of pending changes for this map. 165 | */ 166 | @Unique 167 | protected DoubleBufferedLong2ObjectHashMap getUpdateQueue() { 168 | return this.queue; 169 | } 170 | 171 | /** 172 | * Makes this map a shared copy of another. The shared copy cannot be directly written into. 173 | */ 174 | protected void makeSharedCopy(final DoubleBufferedLong2ObjectHashMap queue) { 175 | this.queue = queue; 176 | this.isShared = true; 177 | 178 | this.queue.flushChangesSync(); 179 | 180 | // Copies of this map should not re-initialize the data structures! 181 | this.init = true; 182 | } 183 | 184 | /** 185 | * Initializes the data for this extended chunk array map. This should only be called once with the initialization 186 | * of a subtype. 187 | * @throws IllegalStateException If the map has already been initialized 188 | */ 189 | @Unique 190 | protected void init() { 191 | if (this.init) { 192 | throw new IllegalStateException("Map already initialized"); 193 | } 194 | 195 | this.queue = new DoubleBufferedLong2ObjectHashMap<>(); 196 | this.init = true; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinLevelPropagator.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import it.unimi.dsi.fastutil.longs.Long2ByteMap; 4 | import net.caffeinemc.phosphor.common.chunk.light.LevelPropagatorAccess; 5 | import net.minecraft.block.BlockState; 6 | import net.minecraft.util.math.MathHelper; 7 | import net.minecraft.world.chunk.light.LevelPropagator; 8 | import org.spongepowered.asm.mixin.Final; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.Unique; 12 | import org.spongepowered.asm.mixin.gen.Invoker; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Redirect; 15 | 16 | @Mixin(LevelPropagator.class) 17 | public abstract class MixinLevelPropagator implements LevelPropagatorAccess { 18 | @Shadow 19 | @Final 20 | private Long2ByteMap pendingUpdates; 21 | 22 | @Shadow 23 | protected abstract int getLevel(long id); 24 | 25 | @Shadow 26 | @Final 27 | private int levelCount; 28 | 29 | @Shadow 30 | protected abstract int getPropagatedLevel(long sourceId, long targetId, int level); 31 | 32 | @Shadow 33 | protected abstract void updateLevel(long sourceId, long id, int level, int currentLevel, int pendingLevel, boolean decrease); 34 | 35 | @Shadow 36 | private volatile boolean hasPendingUpdates; 37 | 38 | @Shadow 39 | private int minPendingLevel; 40 | 41 | @Override 42 | @Invoker("propagateLevel") 43 | public abstract void invokePropagateLevel(long sourceId, long targetId, int level, boolean decrease); 44 | 45 | @Shadow 46 | protected abstract void propagateLevel(long sourceId, long targetId, int level, boolean decrease); 47 | 48 | @Shadow 49 | protected abstract void removePendingUpdate(long id); 50 | 51 | @Override 52 | public void propagateLevel(long sourceId, long targetId, boolean decrease) { 53 | this.propagateLevel(sourceId, targetId, this.getLevel(sourceId), decrease); 54 | } 55 | 56 | @Override 57 | public void checkForUpdates() { 58 | this.hasPendingUpdates = this.minPendingLevel < this.levelCount; 59 | } 60 | 61 | // [VanillaCopy] LevelPropagator#propagateLevel(long, long, int, boolean) 62 | /** 63 | * Mirrors {@link LevelPropagator#propagateLevel(long, int, boolean)}, but allows a block state to be passed to 64 | * prevent subsequent lookup later. 65 | */ 66 | @Unique 67 | protected void propagateLevel(long sourceId, BlockState sourceState, long targetId, int level, boolean decrease) { 68 | int pendingLevel = this.pendingUpdates.get(targetId) & 0xFF; 69 | 70 | int propagatedLevel = this.getPropagatedLevel(sourceId, sourceState, targetId, level); 71 | int clampedLevel = MathHelper.clamp(propagatedLevel, 0, this.levelCount - 1); 72 | 73 | if (decrease) { 74 | this.updateLevel(sourceId, targetId, clampedLevel, this.getLevel(targetId), pendingLevel, true); 75 | 76 | return; 77 | } 78 | 79 | boolean flag; 80 | int resultLevel; 81 | 82 | if (pendingLevel == 0xFF) { 83 | flag = true; 84 | resultLevel = MathHelper.clamp(this.getLevel(targetId), 0, this.levelCount - 1); 85 | } else { 86 | resultLevel = pendingLevel; 87 | flag = false; 88 | } 89 | 90 | if (clampedLevel == resultLevel) { 91 | this.updateLevel(sourceId, targetId, this.levelCount - 1, flag ? resultLevel : this.getLevel(targetId), pendingLevel, false); 92 | } 93 | } 94 | 95 | /** 96 | * Copy of {@link #getPropagatedLevel(long, long, int)} but with an additional argument to pass the 97 | * block state belonging to {@param sourceId}. 98 | */ 99 | @Unique 100 | protected int getPropagatedLevel(long sourceId, BlockState sourceState, long targetId, int level) { 101 | return this.getPropagatedLevel(sourceId, targetId, level); 102 | } 103 | 104 | @Redirect(method = { "removePendingUpdate(JIIZ)V", "applyPendingUpdates" }, at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/longs/Long2ByteMap;remove(J)B", remap = false)) 105 | private byte redirectRemovePendingUpdate(Long2ByteMap map, long key) { 106 | byte ret = map.remove(key); 107 | 108 | if (ret != map.defaultReturnValue()) { 109 | this.onPendingUpdateRemoved(key); 110 | } 111 | 112 | return ret; 113 | } 114 | 115 | @Redirect(method = "addPendingUpdate", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/longs/Long2ByteMap;put(JB)B", remap = false)) 116 | private byte redirectAddPendingUpdate(Long2ByteMap map, long key, byte value) { 117 | byte ret = map.put(key, value); 118 | 119 | if (ret == map.defaultReturnValue()) { 120 | this.onPendingUpdateAdded(key); 121 | } 122 | 123 | return ret; 124 | } 125 | 126 | @Unique 127 | protected void onPendingUpdateAdded(long key) { 128 | // NO-OP 129 | } 130 | 131 | @Unique 132 | protected void onPendingUpdateRemoved(long key) { 133 | // NO-OP 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinLightStorage.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import it.unimi.dsi.fastutil.ints.Int2ByteMap; 4 | import it.unimi.dsi.fastutil.ints.Int2ByteOpenHashMap; 5 | import it.unimi.dsi.fastutil.ints.IntArrayList; 6 | import it.unimi.dsi.fastutil.ints.IntIterable; 7 | import it.unimi.dsi.fastutil.ints.IntIterator; 8 | import it.unimi.dsi.fastutil.ints.IntIterators; 9 | import it.unimi.dsi.fastutil.ints.IntList; 10 | import it.unimi.dsi.fastutil.longs.Long2IntMap; 11 | import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; 12 | import it.unimi.dsi.fastutil.longs.Long2ObjectMap; 13 | import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; 14 | import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; 15 | import it.unimi.dsi.fastutil.longs.LongIterator; 16 | import it.unimi.dsi.fastutil.longs.LongOpenHashSet; 17 | import it.unimi.dsi.fastutil.longs.LongSet; 18 | import it.unimi.dsi.fastutil.objects.ObjectIterator; 19 | import net.caffeinemc.phosphor.common.chunk.light.IReadonly; 20 | import net.caffeinemc.phosphor.common.chunk.light.LevelPropagatorAccess; 21 | import net.caffeinemc.phosphor.common.chunk.light.LightProviderUpdateTracker; 22 | import net.caffeinemc.phosphor.common.chunk.light.LightStorageAccess; 23 | import net.caffeinemc.phosphor.common.util.chunk.light.EmptyChunkNibbleArray; 24 | import net.caffeinemc.phosphor.common.util.math.ChunkSectionPosHelper; 25 | import net.minecraft.util.Util; 26 | import net.minecraft.util.math.BlockPos; 27 | import net.minecraft.util.math.ChunkSectionPos; 28 | import net.minecraft.util.math.Direction; 29 | import net.minecraft.world.LightType; 30 | import net.minecraft.world.SectionDistanceLevelPropagator; 31 | import net.minecraft.world.chunk.ChunkNibbleArray; 32 | import net.minecraft.world.chunk.ChunkProvider; 33 | import net.minecraft.world.chunk.ChunkToNibbleArrayMap; 34 | import net.minecraft.world.chunk.light.ChunkLightProvider; 35 | import net.minecraft.world.chunk.light.LightStorage; 36 | import org.objectweb.asm.Opcodes; 37 | import org.spongepowered.asm.mixin.Final; 38 | import org.spongepowered.asm.mixin.Mixin; 39 | import org.spongepowered.asm.mixin.Mutable; 40 | import org.spongepowered.asm.mixin.Overwrite; 41 | import org.spongepowered.asm.mixin.Shadow; 42 | import org.spongepowered.asm.mixin.Unique; 43 | import org.spongepowered.asm.mixin.gen.Invoker; 44 | import org.spongepowered.asm.mixin.injection.At; 45 | import org.spongepowered.asm.mixin.injection.Inject; 46 | import org.spongepowered.asm.mixin.injection.Redirect; 47 | import org.spongepowered.asm.mixin.injection.Slice; 48 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 49 | 50 | import java.util.concurrent.locks.StampedLock; 51 | import java.util.function.LongPredicate; 52 | 53 | @Mixin(LightStorage.class) 54 | public abstract class MixinLightStorage extends SectionDistanceLevelPropagator implements LightStorageAccess { 55 | protected MixinLightStorage() { 56 | super(0, 0, 0); 57 | } 58 | 59 | @Shadow 60 | @Final 61 | protected ChunkToNibbleArrayMap storage; 62 | 63 | @Mutable 64 | @Shadow 65 | @Final 66 | protected LongSet dirtySections; 67 | 68 | @Mutable 69 | @Shadow 70 | @Final 71 | protected LongSet notifySections; 72 | 73 | @Shadow 74 | protected abstract int getLevel(long id); 75 | 76 | @Mutable 77 | @Shadow 78 | @Final 79 | protected LongSet readySections; 80 | 81 | @Mutable 82 | @Shadow 83 | @Final 84 | protected LongSet markedReadySections; 85 | 86 | @Mutable 87 | @Shadow 88 | @Final 89 | protected LongSet markedNotReadySections; 90 | 91 | @Shadow 92 | protected abstract void onLoadSection(long blockPos); 93 | 94 | @Shadow 95 | protected volatile boolean hasLightUpdates; 96 | 97 | @Shadow 98 | protected volatile ChunkToNibbleArrayMap uncachedStorage; 99 | 100 | @Shadow 101 | protected abstract ChunkNibbleArray createSection(long pos); 102 | 103 | @Shadow 104 | @Final 105 | protected Long2ObjectMap queuedSections; 106 | 107 | @Shadow 108 | protected abstract boolean hasLightUpdates(); 109 | 110 | @Shadow 111 | protected abstract void onUnloadSection(long l); 112 | 113 | @Shadow 114 | @Final 115 | private static Direction[] DIRECTIONS; 116 | 117 | @Shadow 118 | protected abstract void removeSection(ChunkLightProvider storage, long blockChunkPos); 119 | 120 | @Shadow 121 | protected abstract ChunkNibbleArray getLightSection(long sectionPos, boolean cached); 122 | 123 | @Shadow 124 | @Final 125 | private ChunkProvider chunkProvider; 126 | 127 | @Shadow 128 | @Final 129 | private LightType lightType; 130 | 131 | @Shadow 132 | @Final 133 | private LongSet queuedEdgeSections; 134 | 135 | // A multiset of 'interesting' sections per chunk. Determines which sections to iterate over when enabling/disabling chunks, for initial lighting, etc. 136 | // This class tracks non-trivial lightmaps, (scheduled) non-empty chunks and nonOptimizable sections resp. Vanilla lightmaps 137 | @Unique 138 | private final Long2ObjectMap trackedSectionsByChunk = new Long2ObjectOpenHashMap<>(); 139 | 140 | @Unique 141 | protected void trackSection(final long sectionPos) { 142 | this.trackSection(ChunkSectionPos.withZeroY(sectionPos), ChunkSectionPos.unpackY(sectionPos)); 143 | } 144 | 145 | @Unique 146 | protected void trackSection(final long chunkPos, final int y) { 147 | final Int2ByteMap sections = this.trackedSectionsByChunk.computeIfAbsent(chunkPos, __ -> new Int2ByteOpenHashMap()); 148 | sections.put(y, (byte) (sections.get(y) + 1)); 149 | } 150 | 151 | @Unique 152 | protected void untrackSection(final long sectionPos) { 153 | this.untrackSection(ChunkSectionPos.withZeroY(sectionPos), ChunkSectionPos.unpackY(sectionPos)); 154 | } 155 | 156 | @Unique 157 | protected void untrackSection(final long chunkPos, final int y) { 158 | final Int2ByteMap sections = this.trackedSectionsByChunk.get(chunkPos); 159 | 160 | final byte count = (byte) (sections.get(y) - 1); 161 | 162 | if (count == 0) { 163 | sections.remove(y); 164 | 165 | if (sections.isEmpty()) { 166 | this.trackedSectionsByChunk.remove(chunkPos); 167 | } 168 | } else { 169 | sections.put(y, count); 170 | } 171 | } 172 | 173 | @Unique 174 | protected IntIterator getTrackedSections(final long chunkPos) { 175 | final Int2ByteMap sections = this.trackedSectionsByChunk.get(chunkPos); 176 | 177 | return sections == null ? IntIterators.EMPTY_ITERATOR : IntIterators.wrap(sections.keySet().toIntArray()); 178 | } 179 | 180 | /** 181 | * The lock which wraps {@link #uncachedStorage}. Locking should always be 182 | * performed when accessing values in the aforementioned storage. 183 | */ 184 | @Unique 185 | protected final StampedLock uncachedLightArraysLock = new StampedLock(); 186 | 187 | /** 188 | * Replaces the two set of calls to unpack the XYZ coordinates from the input to just one, storing the result as local 189 | * variables. 190 | * 191 | * Additionally, this handles lookups for positions without an associated lightmap. 192 | * 193 | * @reason Use faster implementation 194 | * @author JellySquid 195 | */ 196 | @Overwrite 197 | public int get(long blockPos) { 198 | int x = BlockPos.unpackLongX(blockPos); 199 | int y = BlockPos.unpackLongY(blockPos); 200 | int z = BlockPos.unpackLongZ(blockPos); 201 | 202 | long chunk = ChunkSectionPos.asLong(ChunkSectionPos.getSectionCoord(x), ChunkSectionPos.getSectionCoord(y), ChunkSectionPos.getSectionCoord(z)); 203 | 204 | ChunkNibbleArray array = this.getLightSection(chunk, true); 205 | 206 | if (array == null) { 207 | return this.getLightWithoutLightmap(blockPos); 208 | } 209 | 210 | return array.get(ChunkSectionPos.getLocalCoord(x), ChunkSectionPos.getLocalCoord(y), ChunkSectionPos.getLocalCoord(z)); 211 | } 212 | 213 | /** 214 | * An extremely important optimization is made here in regards to adding items to the pending notification set. The 215 | * original implementation attempts to add the coordinate of every chunk which contains a neighboring block position 216 | * even though a huge number of loop iterations will simply map to block positions within the same updating chunk. 217 | *

218 | * Our implementation here avoids this by pre-calculating the min/max chunk coordinates so we can iterate over only 219 | * the relevant chunk positions once. This reduces what would always be 27 iterations to just 1-8 iterations. 220 | * 221 | * @reason Use faster implementation 222 | * @author JellySquid 223 | */ 224 | @Overwrite 225 | public void set(long blockPos, int value) { 226 | int x = BlockPos.unpackLongX(blockPos); 227 | int y = BlockPos.unpackLongY(blockPos); 228 | int z = BlockPos.unpackLongZ(blockPos); 229 | 230 | long chunkPos = ChunkSectionPos.asLong(x >> 4, y >> 4, z >> 4); 231 | 232 | final ChunkNibbleArray lightmap = this.getOrAddLightmap(chunkPos); 233 | final int oldVal = lightmap.get(x & 15, y & 15, z & 15); 234 | 235 | this.beforeLightChange(blockPos, oldVal, value, lightmap); 236 | this.changeLightmapComplexity(chunkPos, this.getLightmapComplexityChange(blockPos, oldVal, value, lightmap)); 237 | 238 | if (this.dirtySections.add(chunkPos)) { 239 | this.storage.replaceWithCopy(chunkPos); 240 | } 241 | 242 | ChunkNibbleArray nibble = this.getLightSection(chunkPos, true); 243 | nibble.set(x & 15, y & 15, z & 15, value); 244 | 245 | for (int z2 = (z - 1) >> 4; z2 <= (z + 1) >> 4; ++z2) { 246 | for (int x2 = (x - 1) >> 4; x2 <= (x + 1) >> 4; ++x2) { 247 | for (int y2 = (y - 1) >> 4; y2 <= (y + 1) >> 4; ++y2) { 248 | this.notifySections.add(ChunkSectionPos.asLong(x2, y2, z2)); 249 | } 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * @author PhiPro 256 | * @reason Move large parts of the logic to other methods 257 | */ 258 | @Overwrite 259 | public void setLevel(long id, int level) { 260 | int oldLevel = this.getLevel(id); 261 | 262 | if (oldLevel != 0 && level == 0) { 263 | this.readySections.add(id); 264 | this.markedReadySections.remove(id); 265 | } 266 | 267 | if (oldLevel == 0 && level != 0) { 268 | this.readySections.remove(id); 269 | this.markedNotReadySections.remove(id); 270 | this.untrackSection(id); 271 | } 272 | 273 | if (oldLevel >= 2 && level < 2) { 274 | this.nonOptimizableSections.add(id); 275 | 276 | if (this.enabledChunks.contains(ChunkSectionPos.withZeroY(id))) { 277 | if (!this.vanillaLightmapsToRemove.remove(id)) { 278 | if (this.getLightSection(id, true) == null) { 279 | this.storage.put(id, this.createTrivialVanillaLightmap(id)); 280 | this.dirtySections.add(id); 281 | this.storage.clearCache(); 282 | } 283 | 284 | this.trackSection(id); 285 | } 286 | } else { 287 | this.trackSection(id); 288 | } 289 | } 290 | 291 | if (oldLevel < 2 && level >= 2) { 292 | this.nonOptimizableSections.remove(id); 293 | 294 | if (this.enabledChunks.contains(ChunkSectionPos.withZeroY(id))) { 295 | final ChunkNibbleArray lightmap = this.getLightSection(id, true); 296 | 297 | if (lightmap != null && ((IReadonly) lightmap).isReadonly()) { 298 | this.vanillaLightmapsToRemove.add(id); 299 | this.markForLightUpdates(); 300 | } else { 301 | this.untrackSection(id); 302 | } 303 | } else { 304 | this.untrackSection(id); 305 | } 306 | } 307 | } 308 | 309 | /** 310 | * @reason Drastically improve efficiency by making removals O(n) instead of O(16*16*16) 311 | * @author JellySquid 312 | */ 313 | @Inject(method = "removeSection", at = @At("HEAD"), cancellable = true) 314 | protected void preRemoveSection(ChunkLightProvider provider, long pos, CallbackInfo ci) { 315 | if (provider instanceof LightProviderUpdateTracker) { 316 | ((LightProviderUpdateTracker) provider).cancelUpdatesForChunk(pos); 317 | 318 | ci.cancel(); 319 | } 320 | } 321 | 322 | @Override 323 | public void runCleanups() { 324 | this.runCleanups(null); 325 | } 326 | 327 | @Unique 328 | protected void runCleanups(final ChunkLightProvider lightProvider) { 329 | if (!this.hasLightUpdates) { 330 | return; 331 | } 332 | 333 | this.removeTrivialLightmaps(lightProvider); 334 | this.removeVanillaLightmaps(lightProvider); 335 | 336 | if (lightProvider == null) { 337 | this.checkForUpdates(); 338 | } 339 | } 340 | 341 | /** 342 | * @author PhiPro 343 | * @reason Re-implement completely 344 | */ 345 | @Overwrite 346 | public void updateLight(final ChunkLightProvider lightProvider, final boolean doSkylight, final boolean skipEdgeLightPropagation) { 347 | if (!this.hasLightUpdates()) { 348 | return; 349 | } 350 | 351 | this.initializeChunks(); 352 | this.addQueuedLightmaps(lightProvider); 353 | this.runCleanups(lightProvider); 354 | 355 | final LongIterator it; 356 | 357 | if (!skipEdgeLightPropagation) { 358 | it = this.queuedSections.keySet().iterator(); 359 | } else { 360 | it = this.queuedEdgeSections.iterator(); 361 | } 362 | 363 | while (it.hasNext()) { 364 | this.updateSection(lightProvider, it.nextLong()); 365 | } 366 | 367 | this.queuedEdgeSections.clear(); 368 | this.queuedSections.clear(); 369 | 370 | // Vanilla would normally iterate back over the map of light arrays to remove those we worked on, but 371 | // that is unneeded now because we removed them earlier. 372 | 373 | this.hasLightUpdates = false; 374 | } 375 | 376 | /** 377 | * @reason Avoid integer boxing, reduce map lookups and iteration as much as possible 378 | * @author JellySquid 379 | */ 380 | @Overwrite 381 | private void updateSection(final ChunkLightProvider chunkLightProvider, long pos) { 382 | if (this.hasSection(pos)) { 383 | final LevelPropagatorAccess levelPropagator = (LevelPropagatorAccess) chunkLightProvider; 384 | 385 | int x = ChunkSectionPos.getBlockCoord(ChunkSectionPos.unpackX(pos)); 386 | int y = ChunkSectionPos.getBlockCoord(ChunkSectionPos.unpackY(pos)); 387 | int z = ChunkSectionPos.getBlockCoord(ChunkSectionPos.unpackZ(pos)); 388 | 389 | for (Direction dir : DIRECTIONS) { 390 | long adjPos = ChunkSectionPos.offset(pos, dir); 391 | 392 | // Avoid updating initializing chunks unnecessarily 393 | if (this.queuedSections.containsKey(adjPos)) { 394 | continue; 395 | } 396 | 397 | // If there is no light data for this section yet, skip it 398 | if (!this.hasSection(adjPos)) { 399 | continue; 400 | } 401 | 402 | for (int u1 = 0; u1 < 16; ++u1) { 403 | for (int u2 = 0; u2 < 16; ++u2) { 404 | long a; 405 | long b; 406 | 407 | switch (dir) { 408 | case DOWN: 409 | a = BlockPos.asLong(x + u2, y, z + u1); 410 | b = BlockPos.asLong(x + u2, y - 1, z + u1); 411 | break; 412 | case UP: 413 | a = BlockPos.asLong(x + u2, y + 15, z + u1); 414 | b = BlockPos.asLong(x + u2, y + 16, z + u1); 415 | break; 416 | case NORTH: 417 | a = BlockPos.asLong(x + u1, y + u2, z); 418 | b = BlockPos.asLong(x + u1, y + u2, z - 1); 419 | break; 420 | case SOUTH: 421 | a = BlockPos.asLong(x + u1, y + u2, z + 15); 422 | b = BlockPos.asLong(x + u1, y + u2, z + 16); 423 | break; 424 | case WEST: 425 | a = BlockPos.asLong(x, y + u1, z + u2); 426 | b = BlockPos.asLong(x - 1, y + u1, z + u2); 427 | break; 428 | case EAST: 429 | a = BlockPos.asLong(x + 15, y + u1, z + u2); 430 | b = BlockPos.asLong(x + 16, y + u1, z + u2); 431 | break; 432 | default: 433 | continue; 434 | } 435 | 436 | levelPropagator.propagateLevel(a, b, false); 437 | levelPropagator.propagateLevel(b, a, false); 438 | } 439 | } 440 | } 441 | 442 | levelPropagator.checkForUpdates(); 443 | } 444 | } 445 | 446 | /** 447 | * @reason 448 | * @author JellySquid 449 | */ 450 | @Overwrite 451 | public void notifyChanges() { 452 | if (!this.dirtySections.isEmpty()) { 453 | // This could result in changes being flushed to various arrays, so write lock. 454 | long stamp = this.uncachedLightArraysLock.writeLock(); 455 | 456 | try { 457 | // This only performs a shallow copy compared to before 458 | ChunkToNibbleArrayMap map = this.storage.copy(); 459 | map.disableCache(); 460 | 461 | this.uncachedStorage = map; 462 | } finally { 463 | this.uncachedLightArraysLock.unlockWrite(stamp); 464 | } 465 | 466 | this.dirtySections.clear(); 467 | } 468 | 469 | if (!this.notifySections.isEmpty()) { 470 | LongIterator it = this.notifySections.iterator(); 471 | 472 | while(it.hasNext()) { 473 | long pos = it.nextLong(); 474 | 475 | this.chunkProvider.onLightUpdate(this.lightType, ChunkSectionPos.from(pos)); 476 | } 477 | 478 | this.notifySections.clear(); 479 | } 480 | } 481 | 482 | /** 483 | * Returns the light value for a position that does not have an associated lightmap. 484 | * This is analogous to {@link LightStorage#getLight(long)}, but uses the cached light data. 485 | */ 486 | @Unique 487 | protected int getLightWithoutLightmap(final long blockPos) { 488 | return 0; 489 | } 490 | 491 | /** 492 | * This method has an equivalent effect to calling {@link #onLoadSection(long)} on all lightmaps in the specified chunk. 493 | * Additional data can also be set up before enabling the chunk. 494 | */ 495 | @Unique 496 | protected void beforeChunkEnabled(final long chunkPos) { 497 | for (final IntIterator it = this.getTrackedSections(chunkPos); it.hasNext(); ) { 498 | final long sectionPos = ChunkSectionPosHelper.updateYLong(chunkPos, it.nextInt()); 499 | 500 | if (this.hasLightmap(sectionPos)) { 501 | this.onLoadSection(sectionPos); 502 | } 503 | } 504 | } 505 | 506 | /** 507 | * This method has an equivalent effect to calling {@link #onUnloadSection(long)} on all (already removed) lightmaps in the specified chunk. 508 | * Additional data can also be removed upon disabling the chunk. 509 | */ 510 | @Unique 511 | protected void afterChunkDisabled(final long chunkPos, final IntIterable removedLightmaps) { 512 | for (IntIterator it = removedLightmaps.iterator(); it.hasNext(); ) { 513 | this.onUnloadSection(ChunkSectionPosHelper.updateYLong(chunkPos, it.nextInt())); 514 | } 515 | } 516 | 517 | @Unique 518 | protected final LongSet enabledChunks = new LongOpenHashSet(); 519 | @Unique 520 | protected final Long2IntMap lightmapComplexities = Util.make(new Long2IntOpenHashMap(), map -> map.defaultReturnValue(-1)); 521 | 522 | @Unique 523 | private final LongSet markedEnabledChunks = new LongOpenHashSet(); 524 | @Unique 525 | private final LongSet trivialLightmaps = new LongOpenHashSet(); 526 | @Unique 527 | private final LongSet vanillaLightmapsToRemove = new LongOpenHashSet(); 528 | 529 | // This is put here since the relevant methods to overwrite are located in LightStorage 530 | @Unique 531 | protected final LongSet nonOptimizableSections = new LongOpenHashSet(); 532 | 533 | @Unique 534 | protected ChunkNibbleArray getOrAddLightmap(final long sectionPos) { 535 | ChunkNibbleArray lightmap = this.getLightSection(sectionPos, true); 536 | 537 | if (lightmap == null) { 538 | lightmap = this.createSection(sectionPos); 539 | } else { 540 | if (((IReadonly) lightmap).isReadonly()) { 541 | lightmap = lightmap.copy(); 542 | 543 | if (this.vanillaLightmapsToRemove.remove(sectionPos)) { 544 | this.untrackSection(sectionPos); 545 | } 546 | } else { 547 | return lightmap; 548 | } 549 | } 550 | 551 | this.storage.put(sectionPos, lightmap); 552 | this.trackSection(sectionPos); 553 | this.dirtySections.add(sectionPos); 554 | this.storage.clearCache(); 555 | 556 | this.onLoadSection(sectionPos); 557 | this.setLightmapComplexity(sectionPos, 0); 558 | 559 | return lightmap; 560 | } 561 | 562 | @Unique 563 | protected void setLightmapComplexity(final long sectionPos, final int complexity) { 564 | int oldComplexity = this.lightmapComplexities.put(sectionPos, complexity); 565 | 566 | if (oldComplexity == 0) { 567 | this.trivialLightmaps.remove(sectionPos); 568 | } 569 | 570 | if (complexity == 0) { 571 | this.trivialLightmaps.add(sectionPos); 572 | this.markForLightUpdates(); 573 | } 574 | } 575 | 576 | @Unique 577 | private void checkForUpdates() { 578 | this.hasLightUpdates = !this.trivialLightmaps.isEmpty() || !this.vanillaLightmapsToRemove.isEmpty() || !this.markedEnabledChunks.isEmpty() || !this.queuedSections.isEmpty(); 579 | } 580 | 581 | @Unique 582 | private void markForLightUpdates() { 583 | // Avoid volatile writes 584 | if (!this.hasLightUpdates) { 585 | this.hasLightUpdates = true; 586 | } 587 | } 588 | 589 | @Unique 590 | protected void changeLightmapComplexity(final long sectionPos, final int amount) { 591 | int complexity = this.lightmapComplexities.get(sectionPos); 592 | 593 | if (complexity == 0) { 594 | this.trivialLightmaps.remove(sectionPos); 595 | } 596 | 597 | complexity += amount; 598 | this.lightmapComplexities.put(sectionPos, complexity); 599 | 600 | if (complexity == 0) { 601 | this.trivialLightmaps.add(sectionPos); 602 | this.markForLightUpdates(); 603 | } 604 | } 605 | 606 | @Unique 607 | protected ChunkNibbleArray getLightmap(final long sectionPos) { 608 | final ChunkNibbleArray lightmap = this.getLightSection(sectionPos, true); 609 | return lightmap == null || ((IReadonly) lightmap).isReadonly() ? null : lightmap; 610 | } 611 | 612 | @Unique 613 | protected boolean hasLightmap(final long sectionPos) { 614 | return this.getLightmap(sectionPos) != null; 615 | } 616 | 617 | /** 618 | * Set up lightmaps and adjust complexities as needed for the given light change. 619 | * Additional data for the given blockPos itself need to be updated manually, whereas the main lightmap complexity will be updated automatically. 620 | */ 621 | @Unique 622 | protected void beforeLightChange(final long blockPos, final int oldVal, final int newVal, final ChunkNibbleArray lightmap) { 623 | } 624 | 625 | @Unique 626 | protected int getLightmapComplexityChange(final long blockPos, final int oldVal, final int newVal, final ChunkNibbleArray lightmap) { 627 | return 0; 628 | } 629 | 630 | /** 631 | * Set up lightmaps and adjust complexities as needed for the given lightmap change. 632 | * If oldLightmap == null, then {@link #onLoadSection(long)} will be called later on. Otherwise, additional data for the given sectionPos itself need to be updated manually. 633 | * The main lightmap complexity will always be updated automatically. 634 | */ 635 | @Unique 636 | protected void beforeLightmapChange(final long sectionPos, final ChunkNibbleArray oldLightmap, final ChunkNibbleArray newLightmap) { 637 | } 638 | 639 | @Unique 640 | protected int getInitialLightmapComplexity(final long sectionPos, final ChunkNibbleArray lightmap) { 641 | return 0; 642 | } 643 | 644 | @Invoker("hasSection") 645 | @Override 646 | public abstract boolean callHasSection(long sectionPos); 647 | 648 | /** 649 | * Determines whether light updates should be propagated into the given section. 650 | * @author PhiPro 651 | * @reason Method completely changed. Allow child mixins to properly extend this. 652 | */ 653 | @Overwrite 654 | public boolean hasSection(final long sectionPos) { 655 | return this.enabledChunks.contains(ChunkSectionPos.withZeroY(sectionPos)); 656 | } 657 | 658 | @Shadow 659 | protected abstract void setColumnEnabled(long columnPos, boolean enabled); 660 | 661 | /** 662 | * @author PhiPro 663 | * @reason Re-implement completely, ensuring data consistency 664 | */ 665 | @Overwrite 666 | public void setSectionStatus(final long sectionPos, final boolean notReady) { 667 | if (notReady) { 668 | if (this.markedReadySections.remove(sectionPos)) { 669 | this.untrackSection(sectionPos); 670 | } else if (!this.readySections.contains(sectionPos) || !this.markedNotReadySections.add(sectionPos)) { 671 | return; 672 | } 673 | 674 | this.updateLevel(Long.MAX_VALUE, sectionPos, 2, false); 675 | } else { 676 | if (!this.markedNotReadySections.remove(sectionPos)) { 677 | if (this.readySections.contains(sectionPos) || !this.markedReadySections.add(sectionPos)) { 678 | return; 679 | } 680 | 681 | this.trackSection(sectionPos); 682 | } 683 | 684 | this.updateLevel(Long.MAX_VALUE, sectionPos, 0, true); 685 | } 686 | } 687 | 688 | @Override 689 | @Invoker("setColumnEnabled") 690 | public abstract void invokeSetColumnEnabled(final long chunkPos, final boolean enabled); 691 | 692 | @Override 693 | public void enableLightUpdates(final long chunkPos) { 694 | if (!this.enabledChunks.contains(chunkPos)){ 695 | this.markedEnabledChunks.add(chunkPos); 696 | this.markForLightUpdates(); 697 | } 698 | } 699 | 700 | @Unique 701 | private void initializeChunks() { 702 | this.storage.clearCache(); 703 | 704 | for (final LongIterator cit = this.markedEnabledChunks.iterator(); cit.hasNext(); ) { 705 | final long chunkPos = cit.nextLong(); 706 | 707 | // First need to register all lightmaps as this data is needed for calculating the initial complexity 708 | 709 | this.beforeChunkEnabled(chunkPos); 710 | 711 | // Now the initial complexities can be computed 712 | 713 | for (final IntIterator it = this.getTrackedSections(chunkPos); it.hasNext(); ) { 714 | final long sectionPos = ChunkSectionPos.asLong(ChunkSectionPos.unpackX(chunkPos), it.nextInt(), ChunkSectionPos.unpackZ(chunkPos)); 715 | 716 | if (this.hasLightmap(sectionPos)) { 717 | this.setLightmapComplexity(sectionPos, this.getInitialLightmapComplexity(sectionPos, this.getLightSection(sectionPos, true))); 718 | } 719 | } 720 | 721 | // Add lightmaps for vanilla compatibility and try to recover stripped data from vanilla saves 722 | 723 | for (final IntIterator it = this.getTrackedSections(chunkPos); it.hasNext(); ) { 724 | final long sectionPos = ChunkSectionPos.asLong(ChunkSectionPos.unpackX(chunkPos), it.nextInt(), ChunkSectionPos.unpackZ(chunkPos)); 725 | 726 | if (this.nonOptimizableSections.contains(sectionPos) && this.getLightSection(sectionPos, true) == null) { 727 | this.storage.put(sectionPos, this.createInitialVanillaLightmap(sectionPos)); 728 | this.dirtySections.add(sectionPos); 729 | } 730 | } 731 | 732 | this.enabledChunks.add(chunkPos); 733 | } 734 | 735 | this.storage.clearCache(); 736 | 737 | this.markedEnabledChunks.clear(); 738 | } 739 | 740 | @Unique 741 | protected ChunkNibbleArray createInitialVanillaLightmap(final long sectionPos) { 742 | return this.createTrivialVanillaLightmap(sectionPos); 743 | } 744 | 745 | @Unique 746 | protected ChunkNibbleArray createTrivialVanillaLightmap(final long sectionPos) { 747 | return new EmptyChunkNibbleArray(); 748 | } 749 | 750 | @Override 751 | public void disableChunkLight(final long chunkPos, final ChunkLightProvider lightProvider) { 752 | if (this.markedEnabledChunks.remove(chunkPos) || !this.enabledChunks.contains(chunkPos)) { 753 | for (final IntIterator it = this.getTrackedSections(chunkPos); it.hasNext(); ) { 754 | final int y = it.nextInt(); 755 | final long sectionPos = ChunkSectionPos.asLong(ChunkSectionPos.unpackX(chunkPos), y, ChunkSectionPos.unpackZ(chunkPos)); 756 | 757 | if (this.storage.removeChunk(sectionPos) != null) { 758 | this.untrackSection(chunkPos, y); 759 | this.dirtySections.add(sectionPos); 760 | } 761 | } 762 | 763 | this.setColumnEnabled(chunkPos, false); 764 | this.removeBlockData(chunkPos); 765 | } else { 766 | // First need to remove all pending light updates before changing any light value 767 | 768 | for (final IntIterator it = this.getTrackedSections(chunkPos); it.hasNext(); ) { 769 | final long sectionPos = ChunkSectionPos.asLong(ChunkSectionPos.unpackX(chunkPos), it.nextInt(), ChunkSectionPos.unpackZ(chunkPos)); 770 | 771 | if (this.hasSection(sectionPos)) { 772 | this.removeSection(lightProvider, sectionPos); 773 | } 774 | } 775 | 776 | // Now the chunk can be disabled 777 | 778 | this.enabledChunks.remove(chunkPos); 779 | 780 | // Now lightmaps can be removed 781 | 782 | final IntList removedLightmaps = new IntArrayList(); 783 | 784 | for (final IntIterator it = this.getTrackedSections(chunkPos); it.hasNext(); ) { 785 | final int y = it.nextInt(); 786 | final long sectionPos = ChunkSectionPosHelper.updateYLong(chunkPos, y); 787 | 788 | if (this.removeLightmap(sectionPos)) { 789 | removedLightmaps.add(y); 790 | } 791 | } 792 | 793 | // These maps are cleared every update and should hence be small 794 | this.queuedSections.keySet().removeIf((LongPredicate) sectionPos -> ChunkSectionPos.withZeroY(sectionPos) == chunkPos); 795 | this.queuedEdgeSections.removeIf((LongPredicate) sectionPos -> ChunkSectionPos.withZeroY(sectionPos) == chunkPos); 796 | 797 | this.storage.clearCache(); 798 | this.setColumnEnabled(chunkPos, false); 799 | 800 | // Remove all additional data 801 | 802 | this.afterChunkDisabled(chunkPos, removedLightmaps); 803 | this.removeBlockData(chunkPos); 804 | } 805 | } 806 | 807 | /** 808 | * Removes the lightmap associated to the provided sectionPos, but does not call {@link #onUnloadSection(long)} or {@link ChunkToNibbleArrayMap#clearCache()} 809 | * @return Whether a lightmap was removed 810 | */ 811 | @Unique 812 | protected boolean removeLightmap(final long sectionPos) { 813 | if (this.storage.removeChunk(sectionPos) == null) { 814 | return false; 815 | } 816 | 817 | this.dirtySections.add(sectionPos); 818 | 819 | if (this.lightmapComplexities.remove(sectionPos) == -1) { 820 | if (this.vanillaLightmapsToRemove.remove(sectionPos)) { 821 | this.untrackSection(sectionPos); 822 | } 823 | 824 | return false; 825 | } else { 826 | this.trivialLightmaps.remove(sectionPos); 827 | this.untrackSection(sectionPos); 828 | return true; 829 | } 830 | } 831 | 832 | @Unique 833 | private void removeBlockData(final long chunkPos) { 834 | for (final IntIterator it = this.getTrackedSections(chunkPos); it.hasNext(); ) { 835 | this.setSectionStatus(ChunkSectionPosHelper.updateYLong(chunkPos, it.nextInt()), true); 836 | } 837 | } 838 | 839 | @Unique 840 | private void removeTrivialLightmaps(final ChunkLightProvider lightProvider) { 841 | for (final LongIterator it = this.trivialLightmaps.iterator(); it.hasNext(); ) { 842 | final long sectionPos = it.nextLong(); 843 | 844 | this.storage.removeChunk(sectionPos); 845 | this.lightmapComplexities.remove(sectionPos); 846 | this.untrackSection(sectionPos); 847 | this.dirtySections.add(sectionPos); 848 | } 849 | 850 | this.storage.clearCache(); 851 | 852 | // Calling onUnloadSection() after removing all the lightmaps is slightly more efficient 853 | 854 | for (final LongIterator it = this.trivialLightmaps.iterator(); it.hasNext(); ) { 855 | this.onUnloadSection(it.nextLong()); 856 | } 857 | 858 | // Add trivial lightmaps for vanilla compatibility 859 | 860 | for (final LongIterator it = this.trivialLightmaps.iterator(); it.hasNext(); ) { 861 | final long sectionPos = it.nextLong(); 862 | 863 | if (this.nonOptimizableSections.contains(sectionPos)) { 864 | this.storage.put(sectionPos, this.createTrivialVanillaLightmap(sectionPos)); 865 | } 866 | } 867 | 868 | this.storage.clearCache(); 869 | 870 | // Remove pending light updates for sections that no longer support light propagations 871 | 872 | if (lightProvider != null) { 873 | for (final LongIterator it = this.trivialLightmaps.iterator(); it.hasNext(); ) { 874 | final long sectionPos = it.nextLong(); 875 | 876 | if (!this.hasSection(sectionPos)) { 877 | this.removeSection(lightProvider, sectionPos); 878 | } 879 | } 880 | } 881 | 882 | this.trivialLightmaps.clear(); 883 | } 884 | 885 | @Unique 886 | private void removeVanillaLightmaps(final ChunkLightProvider lightProvider) { 887 | for (final LongIterator it = this.vanillaLightmapsToRemove.iterator(); it.hasNext(); ) { 888 | final long sectionPos = it.nextLong(); 889 | 890 | this.storage.removeChunk(sectionPos); 891 | this.untrackSection(sectionPos); 892 | this.dirtySections.add(sectionPos); 893 | } 894 | 895 | this.storage.clearCache(); 896 | 897 | // Remove pending light updates for sections that no longer support light propagations 898 | 899 | if (lightProvider != null) { 900 | for (final LongIterator it = this.vanillaLightmapsToRemove.iterator(); it.hasNext(); ) { 901 | final long sectionPos = it.nextLong(); 902 | 903 | if (!this.hasSection(sectionPos)) { 904 | this.removeSection(lightProvider, sectionPos); 905 | } 906 | } 907 | } 908 | 909 | this.vanillaLightmapsToRemove.clear(); 910 | } 911 | 912 | @Unique 913 | private void addQueuedLightmaps(final ChunkLightProvider lightProvider) { 914 | for (final ObjectIterator> it = Long2ObjectMaps.fastIterator(this.queuedSections); it.hasNext(); ) { 915 | final Long2ObjectMap.Entry entry = it.next(); 916 | 917 | final long sectionPos = entry.getLongKey(); 918 | final ChunkNibbleArray lightmap = entry.getValue(); 919 | 920 | final ChunkNibbleArray oldLightmap = this.getLightmap(sectionPos); 921 | 922 | if (lightmap != oldLightmap) { 923 | this.removeSection(lightProvider, sectionPos); 924 | 925 | this.beforeLightmapChange(sectionPos, oldLightmap, lightmap); 926 | 927 | this.storage.put(sectionPos, lightmap); 928 | this.storage.clearCache(); 929 | this.dirtySections.add(sectionPos); 930 | 931 | if (oldLightmap == null) { 932 | this.trackSection(sectionPos); 933 | this.onLoadSection(sectionPos); 934 | } 935 | 936 | if (this.vanillaLightmapsToRemove.remove(sectionPos)) { 937 | this.untrackSection(sectionPos); 938 | } 939 | 940 | this.setLightmapComplexity(sectionPos, this.getInitialLightmapComplexity(sectionPos, lightmap)); 941 | } 942 | } 943 | } 944 | 945 | /** 946 | * @author PhiPro 947 | * @reason Add lightmaps for disabled chunks directly to the world 948 | */ 949 | @Overwrite 950 | public void enqueueSectionData(final long sectionPos, final ChunkNibbleArray array, final boolean bl) { 951 | final boolean chunkEnabled = this.enabledChunks.contains(ChunkSectionPos.withZeroY(sectionPos)); 952 | 953 | if (array != null) { 954 | if (chunkEnabled) { 955 | this.queuedSections.put(sectionPos, array); 956 | this.markForLightUpdates(); 957 | } else { 958 | this.storage.put(sectionPos, array); 959 | this.trackSection(sectionPos); 960 | this.dirtySections.add(sectionPos); 961 | } 962 | 963 | if (!bl) { 964 | this.queuedEdgeSections.add(sectionPos); 965 | } 966 | } else { 967 | if (chunkEnabled) { 968 | this.queuedSections.remove(sectionPos); 969 | } else { 970 | if (this.storage.removeChunk(sectionPos) != null) { 971 | this.untrackSection(sectionPos); 972 | this.dirtySections.add(sectionPos); 973 | } 974 | } 975 | } 976 | } 977 | 978 | // Queued lightmaps are only added to the world via updateLightmaps() 979 | @Redirect( 980 | method = "createSection(J)Lnet/minecraft/world/chunk/ChunkNibbleArray;", 981 | slice = @Slice( 982 | from = @At( 983 | value = "FIELD", 984 | target = "Lnet/minecraft/world/chunk/light/LightStorage;queuedSections:Lit/unimi/dsi/fastutil/longs/Long2ObjectMap;", 985 | opcode = Opcodes.GETFIELD 986 | ) 987 | ), 988 | at = @At( 989 | value = "INVOKE", 990 | target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectMap;get(J)Ljava/lang/Object;", 991 | ordinal = 0, 992 | remap = false 993 | ) 994 | ) 995 | private Object cancelLightmapLookupFromQueue(final Long2ObjectMap lightmapArray, final long pos) { 996 | return null; 997 | } 998 | 999 | @Redirect( 1000 | method = "getLevel(J)I", 1001 | slice = @Slice( 1002 | from = @At( 1003 | value = "FIELD", 1004 | target = "Lnet/minecraft/world/chunk/light/LightStorage;storage:Lnet/minecraft/world/chunk/ChunkToNibbleArrayMap;", 1005 | opcode = Opcodes.GETFIELD 1006 | ) 1007 | ), 1008 | at = @At( 1009 | value = "INVOKE", 1010 | target = "Lnet/minecraft/world/chunk/ChunkToNibbleArrayMap;containsKey(J)Z", 1011 | ordinal = 0 1012 | ) 1013 | ) 1014 | private boolean isNonOptimizable(final ChunkToNibbleArrayMap lightmapArray, final long sectionPos) { 1015 | return this.nonOptimizableSections.contains(sectionPos); 1016 | } 1017 | } 1018 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinLightingProvider.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.chunk.light.InitialLightingAccess; 4 | import net.minecraft.util.math.BlockPos; 5 | import net.minecraft.util.math.ChunkPos; 6 | import net.minecraft.util.math.ChunkSectionPos; 7 | import net.minecraft.world.HeightLimitView; 8 | import net.minecraft.world.chunk.light.ChunkLightProvider; 9 | import net.minecraft.world.chunk.light.LightingProvider; 10 | import org.spongepowered.asm.mixin.Final; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | 14 | @Mixin(LightingProvider.class) 15 | public abstract class MixinLightingProvider implements InitialLightingAccess 16 | { 17 | @Shadow 18 | @Final 19 | private ChunkLightProvider blockLightProvider; 20 | 21 | @Shadow 22 | @Final 23 | private ChunkLightProvider skyLightProvider; 24 | 25 | @Shadow 26 | public void setSectionStatus(ChunkSectionPos pos, boolean notReady) { 27 | } 28 | 29 | @Shadow 30 | public void setRetainData(ChunkPos pos, boolean retainData) { 31 | } 32 | 33 | @Shadow 34 | public void addLightSource(BlockPos pos, int level) { 35 | } 36 | 37 | @Shadow 38 | @Final 39 | protected HeightLimitView world; 40 | 41 | @Override 42 | public void enableSourceLight(final long chunkPos) { 43 | if (this.blockLightProvider != null) { 44 | ((InitialLightingAccess) this.blockLightProvider).enableSourceLight(chunkPos); 45 | } 46 | 47 | if (this.skyLightProvider != null) { 48 | ((InitialLightingAccess) this.skyLightProvider).enableSourceLight(chunkPos); 49 | } 50 | } 51 | 52 | @Override 53 | public void enableLightUpdates(final long chunkPos) { 54 | if (this.blockLightProvider != null) { 55 | ((InitialLightingAccess) this.blockLightProvider).enableLightUpdates(chunkPos); 56 | } 57 | 58 | if (this.skyLightProvider != null) { 59 | ((InitialLightingAccess) this.skyLightProvider).enableLightUpdates(chunkPos); 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinServerLightingProvider.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import net.caffeinemc.phosphor.common.chunk.light.ServerLightingProviderAccess; 4 | import net.caffeinemc.phosphor.mixin.world.ThreadedAnvilChunkStorageAccess; 5 | import net.minecraft.server.world.ServerLightingProvider; 6 | import net.minecraft.server.world.ThreadedAnvilChunkStorage; 7 | import net.minecraft.util.Util; 8 | import net.minecraft.util.math.ChunkPos; 9 | import net.minecraft.util.math.ChunkSectionPos; 10 | import net.minecraft.world.chunk.Chunk; 11 | import net.minecraft.world.chunk.ChunkSection; 12 | import org.spongepowered.asm.mixin.Final; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Overwrite; 15 | import org.spongepowered.asm.mixin.Shadow; 16 | 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.function.IntSupplier; 19 | 20 | @Mixin(ServerLightingProvider.class) 21 | public abstract class MixinServerLightingProvider extends MixinLightingProvider implements ServerLightingProviderAccess { 22 | @Shadow 23 | protected abstract void enqueue(int x, int z, IntSupplier completedLevelSupplier, ServerLightingProvider.Stage stage, Runnable task); 24 | 25 | @Shadow 26 | protected abstract void enqueue(int x, int z, ServerLightingProvider.Stage stage, Runnable task); 27 | 28 | @Override 29 | public CompletableFuture setupLightmaps(final Chunk chunk) { 30 | final ChunkPos chunkPos = chunk.getPos(); 31 | 32 | // This evaluates the non-empty subchunks concurrently on the lighting thread... 33 | this.enqueue(chunkPos.x, chunkPos.z, () -> 0, ServerLightingProvider.Stage.PRE_UPDATE, Util.debugRunnable(() -> { 34 | final ChunkSection[] chunkSections = chunk.getSectionArray(); 35 | 36 | for (int i = 0; i < chunk.countVerticalSections(); ++i) { 37 | if (!chunkSections[i].isEmpty()) { 38 | super.setSectionStatus(ChunkSectionPos.from(chunkPos, this.world.sectionIndexToCoord(i)), false); 39 | } 40 | } 41 | 42 | if (chunk.isLightOn()) { 43 | super.enableSourceLight(ChunkSectionPos.withZeroY(ChunkSectionPos.asLong(chunkPos.x, 0, chunkPos.z))); 44 | } 45 | 46 | super.enableLightUpdates(ChunkSectionPos.withZeroY(ChunkSectionPos.asLong(chunkPos.x, 0, chunkPos.z))); 47 | }, 48 | () -> "setupLightmaps " + chunkPos 49 | )); 50 | 51 | return CompletableFuture.supplyAsync(() -> { 52 | super.setRetainData(chunkPos, false); 53 | return chunk; 54 | }, 55 | (runnable) -> this.enqueue(chunkPos.x, chunkPos.z, () -> 0, ServerLightingProvider.Stage.POST_UPDATE, runnable) 56 | ); 57 | } 58 | 59 | @Shadow 60 | @Final 61 | private ThreadedAnvilChunkStorage chunkStorage; 62 | 63 | /** 64 | * @author PhiPro 65 | * @reason Move parts of the logic to {@link #setupLightmaps(Chunk)} 66 | */ 67 | @Overwrite 68 | public CompletableFuture light(Chunk chunk, boolean excludeBlocks) { 69 | final ChunkPos chunkPos = chunk.getPos(); 70 | 71 | this.enqueue(chunkPos.x, chunkPos.z, ServerLightingProvider.Stage.PRE_UPDATE, Util.debugRunnable(() -> { 72 | if (!chunk.isLightOn()) { 73 | super.enableSourceLight(ChunkSectionPos.withZeroY(ChunkSectionPos.asLong(chunkPos.x, 0, chunkPos.z))); 74 | } 75 | 76 | if (!excludeBlocks) { 77 | chunk.getLightSourcesStream().forEach(blockPos -> super.addLightSource(blockPos, chunk.getLuminance(blockPos))); 78 | } 79 | }, 80 | () -> "lightChunk " + chunkPos + " " + excludeBlocks 81 | )); 82 | 83 | return CompletableFuture.supplyAsync(() -> { 84 | chunk.setLightOn(true); 85 | ((ThreadedAnvilChunkStorageAccess) this.chunkStorage).invokeReleaseLightTicket(chunkPos); 86 | 87 | return chunk; 88 | }, 89 | (runnable) -> this.enqueue(chunkPos.x, chunkPos.z, ServerLightingProvider.Stage.POST_UPDATE, runnable) 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/chunk/light/MixinSkyLightStorageData.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.chunk.light; 2 | 3 | import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; 4 | import net.caffeinemc.phosphor.common.chunk.light.SharedSkyLightData; 5 | import net.caffeinemc.phosphor.common.chunk.light.SkyLightStorageDataAccess; 6 | import net.caffeinemc.phosphor.common.util.collections.DoubleBufferedLong2IntHashMap; 7 | import net.caffeinemc.phosphor.common.util.collections.DoubleBufferedLong2ObjectHashMap; 8 | import net.minecraft.world.chunk.ChunkNibbleArray; 9 | import net.minecraft.world.chunk.light.SkyLightStorage; 10 | import org.spongepowered.asm.mixin.Final; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Mutable; 13 | import org.spongepowered.asm.mixin.Overwrite; 14 | import org.spongepowered.asm.mixin.Shadow; 15 | import org.spongepowered.asm.mixin.Unique; 16 | 17 | @Mixin(SkyLightStorage.Data.class) 18 | public abstract class MixinSkyLightStorageData extends MixinChunkToNibbleArrayMap 19 | implements SkyLightStorageDataAccess, SharedSkyLightData { 20 | @Shadow 21 | int minSectionY; 22 | 23 | @Mutable 24 | @Shadow 25 | @Final 26 | Long2IntOpenHashMap columnToTopSection; 27 | 28 | // Our new double-buffered collection 29 | @Unique 30 | private DoubleBufferedLong2IntHashMap topArraySectionYQueue; 31 | 32 | @Override 33 | public void makeSharedCopy(final DoubleBufferedLong2ObjectHashMap queue, final DoubleBufferedLong2IntHashMap topSectionQueue) { 34 | this.makeSharedCopy(queue); 35 | 36 | this.topArraySectionYQueue = topSectionQueue; 37 | 38 | // We need to immediately see all updates on the thread this is being copied to 39 | this.topArraySectionYQueue.flushChangesSync(); 40 | } 41 | 42 | /** 43 | * @reason Avoid copying large data structures 44 | * @author JellySquid 45 | */ 46 | @SuppressWarnings("ConstantConditions") 47 | @Overwrite 48 | public SkyLightStorage.Data copy() { 49 | // This will be called immediately by LightStorage in the constructor 50 | // We can take advantage of this fact to initialize our extra properties here without additional hacks 51 | if (!this.isInitialized()) { 52 | this.init(); 53 | } 54 | 55 | SkyLightStorage.Data data = new SkyLightStorage.Data(this.arrays, this.columnToTopSection, this.minSectionY); 56 | ((SharedSkyLightData) (Object) data).makeSharedCopy(this.getUpdateQueue(), this.topArraySectionYQueue); 57 | 58 | return data; 59 | } 60 | 61 | @Override 62 | protected void init() { 63 | super.init(); 64 | 65 | this.topArraySectionYQueue = new DoubleBufferedLong2IntHashMap(); 66 | this.columnToTopSection = this.topArraySectionYQueue.createSyncView(); 67 | } 68 | 69 | @Override 70 | public int getDefaultHeight() { 71 | return this.minSectionY; 72 | } 73 | 74 | @Override 75 | public int getHeight(long pos) { 76 | if (this.isShared()) { 77 | return this.topArraySectionYQueue.getAsync(pos); 78 | } else { 79 | return this.topArraySectionYQueue.getSync(pos); 80 | } 81 | } 82 | 83 | @Override 84 | public void updateMinHeight(final int y) { 85 | this.checkExclusiveOwner(); 86 | 87 | if (this.minSectionY > y) { 88 | this.minSectionY = y; 89 | this.topArraySectionYQueue.defaultReturnValueSync(this.minSectionY); 90 | } 91 | } 92 | 93 | @Override 94 | public void setHeight(final long chunkPos, final int y) { 95 | this.checkExclusiveOwner(); 96 | 97 | if (y > this.minSectionY) { 98 | this.topArraySectionYQueue.putSync(chunkPos, y); 99 | } else { 100 | this.topArraySectionYQueue.removeSync(chunkPos); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/net/caffeinemc/phosphor/mixin/world/ThreadedAnvilChunkStorageAccess.java: -------------------------------------------------------------------------------- 1 | package net.caffeinemc.phosphor.mixin.world; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.gen.Invoker; 5 | import net.minecraft.server.world.ThreadedAnvilChunkStorage; 6 | import net.minecraft.util.math.ChunkPos; 7 | 8 | @Mixin(ThreadedAnvilChunkStorage.class) 9 | public interface ThreadedAnvilChunkStorageAccess { 10 | @Invoker("releaseLightTicket") 11 | void invokeReleaseLightTicket(ChunkPos pos); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/assets/phosphor/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaffeineMC/phosphor-fabric/4e8ed36936e6cef3264f884f40827af605aa389a/src/main/resources/assets/phosphor/icon.png -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "phosphor", 4 | "version": "${version}", 5 | "name": "Phosphor", 6 | "description": "Phosphor is a free and open-source optimization mod for Minecraft which improves the performance of the lighting engine, resulting in significantly reduced world generation times and improved server tick rates. You can help feed me and support development on Patreon, gaining some special perks in the process! Any amount of support helps a ton. https://www.patreon.com/jellysquid", 7 | "authors": [ 8 | "JellySquid", 9 | "PhiPro" 10 | ], 11 | "contact": { 12 | "homepage": "https://caffeinemc.net", 13 | "sources": "https://github.com/CaffeineMC/phosphor-fabric", 14 | "issues": "https://github.com/CaffeineMC/phosphor-fabric/issues" 15 | }, 16 | "license": "LGPL-3.0-only", 17 | "icon": "assets/phosphor/icon.png", 18 | "environment": "*", 19 | "mixins": [ 20 | "phosphor.mixins.json" 21 | ], 22 | "accessWidener": "phosphor.accesswidener", 23 | "depends": { 24 | "fabricloader": ">=0.8.0", 25 | "minecraft": "1.19.x" 26 | }, 27 | "breaks": { 28 | "starlight": "*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/phosphor.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | 3 | accessible class net/minecraft/block/AbstractBlock$AbstractBlockState$ShapeCache 4 | 5 | accessible class net/minecraft/world/chunk/light/BlockLightStorage$Data 6 | accessible class net/minecraft/world/chunk/light/SkyLightStorage$Data 7 | 8 | accessible class net/minecraft/world/chunk/ChunkStatus$GenerationTask 9 | accessible class net/minecraft/world/chunk/ChunkStatus$LoadTask 10 | 11 | accessible class net/minecraft/server/world/ServerLightingProvider$Stage 12 | 13 | extendable class net/minecraft/world/chunk/ChunkNibbleArray 14 | extendable method net/minecraft/world/chunk/ChunkNibbleArray get (I)I 15 | -------------------------------------------------------------------------------- /src/main/resources/phosphor.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "net.caffeinemc.phosphor.mixin", 4 | "compatibilityLevel": "JAVA_17", 5 | "mixins": [ 6 | "block.MixinAbstractBlockState", 7 | "block.MixinShapeCache", 8 | "chunk.MixinChunkNibbleArray", 9 | "chunk.MixinWorldChunk", 10 | "chunk.light.MixinLightingProvider", 11 | "chunk.light.MixinBlockLightStorageData", 12 | "chunk.light.MixinChunkBlockLightProvider", 13 | "chunk.light.MixinChunkLightProvider", 14 | "chunk.light.MixinChunkSkyLightProvider", 15 | "chunk.light.MixinChunkToNibbleArrayMap", 16 | "chunk.light.MixinLevelPropagator", 17 | "chunk.light.MixinLightStorage", 18 | "chunk.light.MixinBlockLightStorage", 19 | "chunk.light.MixinSkyLightStorage", 20 | "chunk.light.MixinSkyLightStorageData", 21 | "chunk.light.MixinServerLightingProvider", 22 | "chunk.MixinProtoChunk", 23 | "world.ThreadedAnvilChunkStorageAccess", 24 | "chunk.MixinChunkStatus" 25 | ], 26 | "injectors": { 27 | "defaultRequire": 1 28 | } 29 | } 30 | --------------------------------------------------------------------------------